五子棋游戏:基于数学模型的AI对战系统

16.1 五子棋游戏概述与数学基础

五子棋是一种历史悠久的传统棋类游戏,起源于古代中国,后来传播至全世界。它的规则简单明了,却蕴含着丰富的策略和思考,成为众多棋类游戏中较为受欢迎的一种。在五子棋中,两名玩家轮流在棋盘交叉点上放置自己颜色的棋子,先将五颗棋子连成一条直线(横、竖或斜线)的玩家获胜。

从数学角度看,五子棋可以看作是一种在二维平面上的序列匹配问题。棋盘可以表示为一个二维矩阵,其中每个元素代表一个交叉点,元素的值表示该点的状态(空、黑棋或白棋)。游戏的胜利条件可以转化为在矩阵中寻找特定模式的序列。

在本章中,我们将使用Unity 2021.3.8f1c1引擎开发一个功能完整的五子棋游戏,包括棋盘渲染、落子交互、胜负判定以及AI对战系统。我们将结合数学原理和游戏开发技术,从零开始构建这个经典游戏。

16.1.1 五子棋的数学模型

从数学角度来看,五子棋可以建模为以下几个方面:

  1. 状态空间:15×15的棋盘共有225个交叉点,每个点有三种可能状态(空、黑棋、白棋),理论上总共有3^225种可能的棋局状态。

  2. 搜索树:每步棋可以视为搜索树中的一个节点,从当前局面到结束局面的所有可能走法构成了一棵巨大的博弈树。

  3. 评估函数:对于任何给定的棋局,我们可以构建数学函数来评估其对各方的有利程度。这通常基于棋子的分布模式、连续性和潜在威胁等因素。

  4. 胜利条件:从数学上讲,五子棋的胜利条件可以表述为在二维矩阵中找到长度为5的相同非零元素的连续序列,可能的方向有水平、垂直和两条对角线。

了解这些数学概念有助于我们更好地设计游戏逻辑和AI算法。接下来,让我们深入了解游戏的详细规则。

16.2 五子棋游戏规则详解

16.2.1 棋盘与棋子的数学表示

传统的五子棋使用15×15的棋盘,共有225个交叉点。在我们的Unity实现中,将使用二维数组来表示棋盘状态:

csharp

public class ChessBoard
{
    // 0表示空位,1表示黑子,2表示白子
    private int[,] boardState;
    
    // 棋盘尺寸
    public const int BOARD_SIZE = 15;
    
    public ChessBoard()
    {
        boardState = new int[BOARD_SIZE, BOARD_SIZE];
        InitializeBoard();
    }
    
    private void InitializeBoard()
    {
        // 初始化为空棋盘
        for (int i = 0; i < BOARD_SIZE; i++)
        {
            for (int j = 0; j < BOARD_SIZE; j++)
            {
                boardState[i, j] = 0;
            }
        }
    }
    
    // 获取指定位置的棋子状态
    public int GetChessPiece(int x, int y)
    {
        if (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE)
        {
            return boardState[x, y];
        }
        return -1; // 越界
    }
    
    // 放置棋子
    public bool PlaceChessPiece(int x, int y, int pieceType)
    {
        if (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE && boardState[x, y] == 0)
        {
            boardState[x, y] = pieceType;
            return true;
        }
        return false;
    }
    
    // 清除棋盘
    public void ClearBoard()
    {
        InitializeBoard();
    }
}

这段代码定义了棋盘类及其基本操作。我们使用二维数组boardState来表示棋盘状态,其中0表示空位,1表示黑子,2表示白子。通过GetChessPiecePlaceChessPiece方法,我们可以查询和修改棋盘上特定位置的状态。

16.2.2 五子棋基本规则系统

五子棋的基本规则相对简单,但在程序实现时需要考虑各种情况。以下是五子棋的核心规则:

  1. 黑方先行,双方轮流在棋盘空位上放置自己的棋子。
  2. 棋子一旦放置不能移动或移除。
  3. 任何一方在任意方向(水平、垂直或对角线)连成五个或更多同色棋子时获胜。
  4. 如果棋盘已满但没有一方达成连五,则游戏为平局。

这些规则可以转化为程序逻辑:

csharp

public class GomokuRules
{
    // 判断游戏是否结束,返回赢家(0:未结束,1:黑方胜,2:白方胜,3:平局)
    public static int CheckGameOver(ChessBoard board)
    {
        // 检查是否有一方连成五子
        for (int i = 0; i < ChessBoard.BOARD_SIZE; i++)
        {
            for (int j = 0; j < ChessBoard.BOARD_SIZE; j++)
            {
                int piece = board.GetChessPiece(i, j);
                if (piece != 0)
                {
                    // 检查水平方向
                    if (CheckFiveInARow(board, i, j, 1, 0))
                        return piece;
                    
                    // 检查垂直方向
                    if (CheckFiveInARow(board, i, j, 0, 1))
                        return piece;
                    
                    // 检查右下对角线
                    if (CheckFiveInARow(board, i, j, 1, 1))
                        return piece;
                    
                    // 检查左下对角线
                    if (CheckFiveInARow(board, i, j, -1, 1))
                        return piece;
                }
            }
        }
        
        // 检查是否棋盘已满(平局)
        bool isFull = true;
        for (int i = 0; i < ChessBoard.BOARD_SIZE; i++)
        {
            for (int j = 0; j < ChessBoard.BOARD_SIZE; j++)
            {
                if (board.GetChessPiece(i, j) == 0)
                {
                    isFull = false;
                    break;
                }
            }
            if (!isFull)
                break;
        }
        
        return isFull ? 3 : 0;
    }
    
    // 检查从(x,y)开始,在(dx,dy)方向上是否有五个相同的棋子
    private static bool CheckFiveInARow(ChessBoard board, int x, int y, int dx, int dy)
    {
        int piece = board.GetChessPiece(x, y);
        if (piece == 0)
            return false;
            
        int count = 1;
        
        // 向前检查
        for (int i = 1; i < 5; i++)
        {
            int nextX = x + i * dx;
            int nextY = y + i * dy;
            
            if (nextX < 0 || nextX >= ChessBoard.BOARD_SIZE || nextY < 0 || nextY >= ChessBoard.BOARD_SIZE)
                break;
                
            if (board.GetChessPiece(nextX, nextY) != piece)
                break;
                
            count++;
        }
        
        // 已经找到五连,不需要继续检查
        if (count >= 5)
            return true;
            
        // 向后检查
        for (int i = 1; i < 5; i++)
        {
            int nextX = x - i * dx;
            int nextY = y - i * dy;
            
            if (nextX < 0 || nextX >= ChessBoard.BOARD_SIZE || nextY < 0 || nextY >= ChessBoard.BOARD_SIZE)
                break;
                
            if (board.GetChessPiece(nextX, nextY) != piece)
                break;
                
            count++;
        }
        
        return count >= 5;
    }
}

这段代码定义了五子棋规则类,其中CheckGameOver方法检查游戏是否结束并返回赢家。它通过调用CheckFiveInARow方法检查从每个有棋子的位置开始,在四个不同方向上是否有五个相同的棋子连成一线。

16.2.3 落子顺序与玩家交替

在五子棋中,黑方先行,然后双方轮流下棋。我们可以用一个简单的状态变量来跟踪当前轮到哪一方:

csharp

public class GameManager : MonoBehaviour
{
    // 当前玩家(1:黑方,2:白方)
    private int currentPlayer = 1;
    
    // 切换当前玩家
    private void SwitchPlayer()
    {
        currentPlayer = currentPlayer == 1 ? 2 : 1;
    }
    
    // 处理玩家落子
    public bool HandlePlayerMove(int x, int y)
    {
        if (chessBoard.PlaceChessPiece(x, y, currentPlayer))
        {
            // 检查游戏是否结束
            int gameResult = GomokuRules.CheckGameOver(chessBoard);
            if (gameResult != 0)
            {
                HandleGameOver(gameResult);
                return true;
            }
            
            // 切换玩家
            SwitchPlayer();
            return true;
        }
        return false;
    }
    
    // 处理游戏结束
    private void HandleGameOver(int result)
    {
        gameIsOver = true;
        
        switch (result)
        {
            case 1:
                Debug.Log("黑方胜利!");
                break;
            case 2:
                Debug.Log("白方胜利!");
                break;
            case 3:
                Debug.Log("平局!");
                break;
        }
    }
}

这段代码展示了如何管理玩家交替落子的逻辑。currentPlayer变量跟踪当前轮到哪一方,SwitchPlayer方法在每次有效落子后切换玩家,而HandlePlayerMove方法处理玩家的落子请求并检查游戏是否结束。

16.2.4 禁手规则实现

在标准五子棋规则中,对黑方有禁手规则,包括三三禁手、四四禁手和长连禁手。禁手是指黑方不能下出形成特定棋型的棋子,否则判定为犯规,白方获胜。

以下是禁手规则的实现:

csharp

public class ForbiddenMoveChecker
{
    private ChessBoard board;
    
    public ForbiddenMoveChecker(ChessBoard board)
    {
        this.board = board;
    }
    
    // 检查位置(x,y)是否是黑方的禁手点
    public bool IsForbiddenMove(int x, int y)
    {
        // 首先检查该位置是否为空
        if (board.GetChessPiece(x, y) != 0)
            return false;
            
        // 临时放置一个黑子
        board.PlaceChessPiece(x, y, 1);
        
        // 检查是否形成长连(超过五子相连)
        bool isOverline = CheckOverline(x, y);
        
        // 检查是否形成三三禁手
        bool isDoubleThree = CheckDoubleThree(x, y);
        
        // 检查是否形成四四禁手
        bool isDoubleFour = CheckDoubleFour(x, y);
        
        // 移除临时放置的黑子
        board.PlaceChessPiece(x, y, 0);
        
        return isOverline || isDoubleThree || isDoubleFour;
    }
    
    // 检查长连禁手(超过五子相连)
    private bool CheckOverline(int x, int y)
    {
        // 检查四个方向(水平、垂直、两条对角线)
        int[][] directions = new int[][]
        {
            new int[] {1, 0},  // 水平
            new int[] {0, 1},  // 垂直
            new int[] {1, 1},  // 右下对角线
            new int[] {-1, 1}  // 左下对角线
        };
        
        foreach (int[] direction in directions)
        {
            int count = 1;
            int dx = direction[0];
            int dy = direction[1];
            
            // 向一个方向计数
            for (int i = 1; i < 6; i++)
            {
                int nx = x + dx * i;
                int ny = y + dy * i;
                
                if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                    break;
                    
                if (board.GetChessPiece(nx, ny) != 1)
                    break;
                    
                count++;
            }
            
            // 向反方向计数
            for (int i = 1; i < 6; i++)
            {
                int nx = x - dx * i;
                int ny = y - dy * i;
                
                if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                    break;
                    
                if (board.GetChessPiece(nx, ny) != 1)
                    break;
                    
                count++;
            }
            
            // 如果连子数大于5,则是长连禁手
            if (count > 5)
                return true;
        }
        
        return false;
    }
    
    // 检查三三禁手(形成两个或更多活三)
    private bool CheckDoubleThree(int x, int y)
    {
        int openThreeCount = 0;
        
        // 检查四个方向(水平、垂直、两条对角线)
        int[][] directions = new int[][]
        {
            new int[] {1, 0},  // 水平
            new int[] {0, 1},  // 垂直
            new int[] {1, 1},  // 右下对角线
            new int[] {-1, 1}  // 左下对角线
        };
        
        foreach (int[] direction in directions)
        {
            int dx = direction[0];
            int dy = direction[1];
            
            // 检查在该方向上是否形成活三
            if (IsOpenThree(x, y, dx, dy))
                openThreeCount++;
                
            // 如果找到两个活三,则是三三禁手
            if (openThreeCount >= 2)
                return true;
        }
        
        return false;
    }
    
    // 检查是否形成活三(两端开放的连续三子)
    private bool IsOpenThree(int x, int y, int dx, int dy)
    {
        // 活三的模式匹配需要复杂的棋型分析
        // 这里简化为检查是否存在形如"_XXX_"的模式,其中X是黑子,_是空位
        
        // 这只是一个简化实现,实际的活三检测要复杂得多
        int[] line = GetLine(x, y, dx, dy);
        
        // 在line数组中寻找"_XXX_"模式
        for (int i = 0; i < line.Length - 4; i++)
        {
            if (line[i] == 0 && line[i+1] == 1 && line[i+2] == 1 && line[i+3] == 1 && line[i+4] == 0)
                return true;
        }
        
        return false;
    }
    
    // 检查四四禁手(形成两个或更多活四/冲四)
    private bool CheckDoubleFour(int x, int y)
    {
        int fourCount = 0;
        
        // 检查四个方向(水平、垂直、两条对角线)
        int[][] directions = new int[][]
        {
            new int[] {1, 0},  // 水平
            new int[] {0, 1},  // 垂直
            new int[] {1, 1},  // 右下对角线
            new int[] {-1, 1}  // 左下对角线
        };
        
        foreach (int[] direction in directions)
        {
            int dx = direction[0];
            int dy = direction[1];
            
            // 检查在该方向上是否形成四
            if (IsFour(x, y, dx, dy))
                fourCount++;
                
            // 如果找到两个四,则是四四禁手
            if (fourCount >= 2)
                return true;
        }
        
        return false;
    }
    
    // 检查是否形成四(包括活四和冲四)
    private bool IsFour(int x, int y, int dx, int dy)
    {
        // 这只是一个简化实现,实际的四检测要复杂得多
        int[] line = GetLine(x, y, dx, dy);
        
        // 在line数组中寻找形成四的模式
        // 活四:"_XXXX_"
        // 冲四:"XXXX_" 或 "_XXXX" 等
        for (int i = 0; i < line.Length - 4; i++)
        {
            if (line[i] == 0 && line[i+1] == 1 && line[i+2] == 1 && line[i+3] == 1 && line[i+4] == 1)
                return true;
            
            if (line[i] == 1 && line[i+1] == 1 && line[i+2] == 1 && line[i+3] == 1 && line[i+4] == 0)
                return true;
        }
        
        return false;
    }
    
    // 获取从(x,y)开始在(dx,dy)方向上的一条线
    private int[] GetLine(int x, int y, int dx, int dy)
    {
        List<int> line = new List<int>();
        
        // 向一个方向
        for (int i = -5; i <= 5; i++)
        {
            int nx = x + dx * i;
            int ny = y + dy * i;
            
            if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                continue;
                
            line.Add(board.GetChessPiece(nx, ny));
        }
        
        return line.ToArray();
    }
}

这段代码实现了五子棋的禁手规则检查。IsForbiddenMove方法检查给定位置是否是黑方的禁手点,它通过调用CheckOverlineCheckDoubleThreeCheckDoubleFour方法检查长连、三三禁手和四四禁手。这些方法使用模式匹配来检查特定的棋型。

需要注意的是,这是一个简化版的实现。实际的禁手检查会更加复杂,需要考虑更多的棋型和边界情况。

16.3 游戏算法思路与实现

16.3.1 棋盘表示与数据结构

在前面的章节中,我们已经介绍了使用二维数组表示棋盘的基本方法。现在,让我们扩展这个概念,构建一个更完整的棋盘类,包括绘制和交互功能:

csharp

public class ChessBoardRenderer : MonoBehaviour
{
    // 棋盘逻辑
    private ChessBoard chessBoard;
    
    // 棋盘网格线
    public GameObject gridLinePrefab;
    
    // 棋子预制体
    public GameObject blackPiecePrefab;
    public GameObject whitePiecePrefab;
    
    // 棋盘尺寸和间距
    public float boardSize = 14f;
    public float cellSize = 1f;
    
    // 棋子字典(用于快速查找和更新)
    private Dictionary<Vector2Int, GameObject> pieceObjects = new Dictionary<Vector2Int, GameObject>();
    
    // 初始化
    void Start()
    {
        chessBoard = new ChessBoard();
        DrawBoard();
    }
    
    // 绘制棋盘
    private void DrawBoard()
    {
        // 创建棋盘背景
        GameObject boardBackground = GameObject.CreatePrimitive(PrimitiveType.Quad);
        boardBackground.transform.parent = transform;
        boardBackground.transform.localPosition = Vector3.zero;
        boardBackground.transform.localScale = new Vector3(boardSize, boardSize, 1);
        
        // 设置背景材质
        Renderer renderer = boardBackground.GetComponent<Renderer>();
        renderer.material.color = new Color(0.9f, 0.7f, 0.4f); // 木质色
        
        // 创建网格线
        for (int i = 0; i < ChessBoard.BOARD_SIZE; i++)
        {
            // 水平线
            GameObject hLine = Instantiate(gridLinePrefab, transform);
            hLine.transform.localPosition = new Vector3(0, i - (ChessBoard.BOARD_SIZE - 1) / 2f, -0.01f);
            hLine.transform.localScale = new Vector3(boardSize - 0.1f, 0.05f, 1);
            
            // 垂直线
            GameObject vLine = Instantiate(gridLinePrefab, transform);
            vLine.transform.localPosition = new Vector3(i - (ChessBoard.BOARD_SIZE - 1) / 2f, 0, -0.01f);
            vLine.transform.localScale = new Vector3(0.05f, boardSize - 0.1f, 1);
        }
        
        // 添加五个标记点(天元和四星)
        CreateMark(7, 7); // 天元
        CreateMark(3, 3); // 左上星
        CreateMark(11, 3); // 右上星
        CreateMark(3, 11); // 左下星
        CreateMark(11, 11); // 右下星
    }
    
    // 创建标记点
    private void CreateMark(int x, int y)
    {
        GameObject mark = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        mark.transform.parent = transform;
        
        float posX = x - (ChessBoard.BOARD_SIZE - 1) / 2f;
        float posY = y - (ChessBoard.BOARD_SIZE - 1) / 2f;
        
        mark.transform.localPosition = new Vector3(posX, posY, -0.02f);
        mark.transform.localScale = new Vector3(0.2f, 0.2f, 0.2f);
        
        Renderer renderer = mark.GetComponent<Renderer>();
        renderer.material.color = Color.black;
    }
    
    // 放置棋子
    public bool PlacePiece(int x, int y, int pieceType)
    {
        if (chessBoard.PlaceChessPiece(x, y, pieceType))
        {
            CreatePieceObject(x, y, pieceType);
            return true;
        }
        return false;
    }
    
    // 创建棋子对象
    private void CreatePieceObject(int x, int y, int pieceType)
    {
        // 选择合适的预制体
        GameObject prefab = pieceType == 1 ? blackPiecePrefab : whitePiecePrefab;
        
        // 实例化棋子
        GameObject piece = Instantiate(prefab, transform);
        
        // 计算位置
        float posX = x - (ChessBoard.BOARD_SIZE - 1) / 2f;
        float posY = y - (ChessBoard.BOARD_SIZE - 1) / 2f;
        
        piece.transform.localPosition = new Vector3(posX, posY, -0.05f);
        
        // 添加到字典
        pieceObjects[new Vector2Int(x, y)] = piece;
    }
    
    // 清除棋盘上的所有棋子
    public void ClearBoard()
    {
        chessBoard.ClearBoard();
        
        // 销毁所有棋子对象
        foreach (GameObject piece in pieceObjects.Values)
        {
            Destroy(piece);
        }
        
        pieceObjects.Clear();
    }
    
    // 将屏幕坐标转换为棋盘坐标
    public Vector2Int ScreenToBoard(Vector2 screenPosition)
    {
        Vector3 worldPosition = Camera.main.ScreenToWorldPoint(new Vector3(screenPosition.x, screenPosition.y, 10));
        Vector3 localPosition = transform.InverseTransformPoint(worldPosition);
        
        int x = Mathf.RoundToInt(localPosition.x + (ChessBoard.BOARD_SIZE - 1) / 2f);
        int y = Mathf.RoundToInt(localPosition.y + (ChessBoard.BOARD_SIZE - 1) / 2f);
        
        // 确保坐标在有效范围内
        x = Mathf.Clamp(x, 0, ChessBoard.BOARD_SIZE - 1);
        y = Mathf.Clamp(y, 0, ChessBoard.BOARD_SIZE - 1);
        
        return new Vector2Int(x, y);
    }
    
    // 获取棋盘逻辑实例
    public ChessBoard GetBoard()
    {
        return chessBoard;
    }
}

这个ChessBoardRenderer类负责棋盘的可视化表示和交互。它创建棋盘背景、网格线和标记点,并在玩家落子时创建相应的棋子对象。它还提供了将屏幕坐标转换为棋盘坐标的功能,以便处理用户输入。

16.3.2 棋子绘制与用户交互

在上一节中,我们已经实现了棋盘和棋子的渲染。现在,让我们实现用户交互,使玩家能够通过点击放置棋子:

csharp

public class GameController : MonoBehaviour
{
    public ChessBoardRenderer boardRenderer;
    private GameManager gameManager;
    private ForbiddenMoveChecker forbiddenMoveChecker;
    
    // UI元素
    public Text statusText;
    public GameObject gameOverPanel;
    public Text winnerText;
    
    // 初始化
    void Start()
    {
        gameManager = new GameManager();
        forbiddenMoveChecker = new ForbiddenMoveChecker(boardRenderer.GetBoard());
        
        // 隐藏游戏结束面板
        gameOverPanel.SetActive(false);
        
        // 更新状态文本
        UpdateStatusText();
    }
    
    // 更新
    void Update()
    {
        // 如果游戏已结束,不处理输入
        if (gameManager.IsGameOver())
            return;
            
        // 处理鼠标点击
        if (Input.GetMouseButtonDown(0))
        {
            // 将屏幕坐标转换为棋盘坐标
            Vector2Int boardPos = boardRenderer.ScreenToBoard(Input.mousePosition);
            
            // 检查是否是禁手点(只对黑方检查)
            if (gameManager.GetCurrentPlayer() == 1 && forbiddenMoveChecker.IsForbiddenMove(boardPos.x, boardPos.y))
            {
                // 显示禁手提示
                statusText.text = "禁手位置,请选择其他位置";
                return;
            }
            
            // 尝试放置棋子
            if (boardRenderer.PlacePiece(boardPos.x, boardPos.y, gameManager.GetCurrentPlayer()))
            {
                // 处理移动并检查游戏是否结束
                bool gameOver = gameManager.HandlePlayerMove(boardPos.x, boardPos.y);
                
                if (gameOver)
                {
                    // 显示游戏结束面板
                    ShowGameOverPanel();
                }
                else
                {
                    // 更新状态文本
                    UpdateStatusText();
                }
            }
        }
    }
    
    // 更新状态文本
    private void UpdateStatusText()
    {
        if (gameManager.GetCurrentPlayer() == 1)
        {
            statusText.text = "黑方回合";
        }
        else
        {
            statusText.text = "白方回合";
        }
    }
    
    // 显示游戏结束面板
    private void ShowGameOverPanel()
    {
        gameOverPanel.SetActive(true);
        
        int winner = gameManager.GetWinner();
        
        switch (winner)
        {
            case 1:
                winnerText.text = "黑方胜利!";
                break;
            case 2:
                winnerText.text = "白方胜利!";
                break;
            case 3:
                winnerText.text = "平局!";
                break;
        }
    }
    
    // 重新开始游戏
    public void RestartGame()
    {
        // 清除棋盘
        boardRenderer.ClearBoard();
        
        // 重置游戏管理器
        gameManager.ResetGame();
        
        // 隐藏游戏结束面板
        gameOverPanel.SetActive(false);
        
        // 更新状态文本
        UpdateStatusText();
    }
}

这个GameController类处理游戏的用户输入和UI更新。它检测鼠标点击,将点击位置转换为棋盘坐标,并尝试放置棋子。它还处理禁手规则,并在游戏结束时显示相应的界面。

16.3.3 落子与游戏流程控制

现在让我们扩展GameManager类,添加更多的游戏流程控制功能:

csharp

public class GameManager
{
    // 当前玩家(1:黑方,2:白方)
    private int currentPlayer = 1;
    
    // 游戏是否结束
    private bool gameIsOver = false;
    
    // 赢家(0:未结束,1:黑方,2:白方,3:平局)
    private int winner = 0;
    
    // 棋盘引用
    private ChessBoard board;
    
    // 移动历史记录,用于悔棋
    private List<Move> moveHistory = new List<Move>();
    
    // 构造函数
    public GameManager()
    {
        board = new ChessBoard();
    }
    
    // 设置棋盘引用
    public void SetBoard(ChessBoard board)
    {
        this.board = board;
    }
    
    // 获取当前玩家
    public int GetCurrentPlayer()
    {
        return currentPlayer;
    }
    
    // 获取赢家
    public int GetWinner()
    {
        return winner;
    }
    
    // 游戏是否结束
    public bool IsGameOver()
    {
        return gameIsOver;
    }
    
    // 切换当前玩家
    private void SwitchPlayer()
    {
        currentPlayer = currentPlayer == 1 ? 2 : 1;
    }
    
    // 处理玩家落子
    public bool HandlePlayerMove(int x, int y)
    {
        // 如果游戏已结束,不允许落子
        if (gameIsOver)
            return false;
            
        // 记录移动
        moveHistory.Add(new Move(x, y, currentPlayer));
        
        // 检查游戏是否结束
        int gameResult = GomokuRules.CheckGameOver(board);
        
        if (gameResult != 0)
        {
            HandleGameOver(gameResult);
            return true;
        }
        
        // 切换玩家
        SwitchPlayer();
        return false;
    }
    
    // 处理游戏结束
    private void HandleGameOver(int result)
    {
        gameIsOver = true;
        winner = result;
    }
    
    // 重置游戏
    public void ResetGame()
    {
        currentPlayer = 1;
        gameIsOver = false;
        winner = 0;
        moveHistory.Clear();
    }
    
    // 悔棋
    public bool UndoMove()
    {
        if (moveHistory.Count == 0)
            return false;
            
        // 获取最后一步
        Move lastMove = moveHistory[moveHistory.Count - 1];
        moveHistory.RemoveAt(moveHistory.Count - 1);
        
        // 移除棋子
        board.PlaceChessPiece(lastMove.X, lastMove.Y, 0);
        
        // 切换回前一个玩家
        currentPlayer = lastMove.Player;
        
        // 重置游戏状态
        gameIsOver = false;
        winner = 0;
        
        return true;
    }
    
    // 移动类
    private class Move
    {
        public int X { get; private set; }
        public int Y { get; private set; }
        public int Player { get; private set; }
        
        public Move(int x, int y, int player)
        {
            X = x;
            Y = y;
            Player = player;
        }
    }
}

这个扩展版的GameManager类增加了更多功能,包括跟踪游戏状态、记录移动历史和支持悔棋。它使用Move类来表示棋盘上的一步移动,包含棋子的坐标和玩家信息。

16.3.4 胜负规则判定与算法

在前面的章节中,我们已经实现了基本的胜负判定逻辑。现在,让我们深入讨论这个算法并优化它:

csharp

public class GomokuRules
{
    // 方向数组:水平、垂直、右下对角线、左下对角线
    private static readonly int[][] directions = new int[][]
    {
        new int[] {1, 0},
        new int[] {0, 1},
        new int[] {1, 1},
        new int[] {-1, 1}
    };
    
    // 判断游戏是否结束,返回赢家(0:未结束,1:黑方胜,2:白方胜,3:平局)
    public static int CheckGameOver(ChessBoard board)
    {
        // 检查是否有一方获胜
        for (int x = 0; x < ChessBoard.BOARD_SIZE; x++)
        {
            for (int y = 0; y < ChessBoard.BOARD_SIZE; y++)
            {
                int piece = board.GetChessPiece(x, y);
                if (piece == 0)
                    continue;
                    
                // 检查各个方向
                foreach (int[] dir in directions)
                {
                    int dx = dir[0];
                    int dy = dir[1];
                    
                    // 检查是否形成五连
                    if (CountConsecutivePieces(board, x, y, dx, dy) >= 5)
                        return piece;
                }
            }
        }
        
        // 检查是否棋盘已满(平局)
        bool isFull = true;
        for (int x = 0; x < ChessBoard.BOARD_SIZE; x++)
        {
            for (int y = 0; y < ChessBoard.BOARD_SIZE; y++)
            {
                if (board.GetChessPiece(x, y) == 0)
                {
                    isFull = false;
                    break;
                }
            }
            if (!isFull)
                break;
        }
        
        return isFull ? 3 : 0;
    }
    
    // 计算从(x,y)开始在(dx,dy)方向上的连续相同棋子数
    private static int CountConsecutivePieces(ChessBoard board, int x, int y, int dx, int dy)
    {
        int piece = board.GetChessPiece(x, y);
        if (piece == 0)
            return 0;
            
        int count = 1;
        
        // 向一个方向计数
        for (int i = 1; i < 5; i++)
        {
            int nx = x + dx * i;
            int ny = y + dy * i;
            
            if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                break;
                
            if (board.GetChessPiece(nx, ny) != piece)
                break;
                
            count++;
        }
        
        // 向反方向计数
        for (int i = 1; i < 5; i++)
        {
            int nx = x - dx * i;
            int ny = y - dy * i;
            
            if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                break;
                
            if (board.GetChessPiece(nx, ny) != piece)
                break;
                
            count++;
        }
        
        return count;
    }
    
    // 检查是否形成特定的棋型(如活三、冲四等)
    public static bool CheckPattern(ChessBoard board, int x, int y, int pieceType, PatternType patternType)
    {
        // 临时放置棋子
        int originalPiece = board.GetChessPiece(x, y);
        board.PlaceChessPiece(x, y, pieceType);
        
        bool result = false;
        
        // 检查各个方向
        foreach (int[] dir in directions)
        {
            int dx = dir[0];
            int dy = dir[1];
            
            // 获取该方向的棋型
            int[] pattern = GetLinePattern(board, x, y, dx, dy);
            
            // 根据patternType检查特定棋型
            switch (patternType)
            {
                case PatternType.OpenFour:
                    if (MatchOpenFour(pattern, pieceType))
                    {
                        result = true;
                        break;
                    }
                    break;
                case PatternType.OpenThree:
                    if (MatchOpenThree(pattern, pieceType))
                    {
                        result = true;
                        break;
                    }
                    break;
                // 可以添加更多的棋型检查...
            }
            
            if (result)
                break;
        }
        
        // 恢复原来的棋子
        board.PlaceChessPiece(x, y, originalPiece);
        
        return result;
    }
    
    // 获取从(x,y)开始在(dx,dy)方向上的棋型
    private static int[] GetLinePattern(ChessBoard board, int x, int y, int dx, int dy)
    {
        List<int> pattern = new List<int>();
        
        // 向一个方向
        for (int i = -5; i <= 5; i++)
        {
            int nx = x + dx * i;
            int ny = y + dy * i;
            
            if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                continue;
                
            pattern.Add(board.GetChessPiece(nx, ny));
        }
        
        return pattern.ToArray();
    }
    
    // 匹配活四模式
    private static bool MatchOpenFour(int[] pattern, int pieceType)
    {
        // 活四:_XXXX_ (其中X是pieceType,_是空位)
        for (int i = 0; i < pattern.Length - 5; i++)
        {
            if (pattern[i] == 0 &&
                pattern[i+1] == pieceType &&
                pattern[i+2] == pieceType &&
                pattern[i+3] == pieceType &&
                pattern[i+4] == pieceType &&
                pattern[i+5] == 0)
            {
                return true;
            }
        }
        
        return false;
    }
    
    // 匹配活三模式
    private static bool MatchOpenThree(int[] pattern, int pieceType)
    {
        // 活三:_XXX_ (其中X是pieceType,_是空位)
        for (int i = 0; i < pattern.Length - 4; i++)
        {
            if (pattern[i] == 0 &&
                pattern[i+1] == pieceType &&
                pattern[i+2] == pieceType &&
                pattern[i+3] == pieceType &&
                pattern[i+4] == 0)
            {
                return true;
            }
        }
        
        return false;
    }
    
    // 棋型枚举
    public enum PatternType
    {
        OpenFour,   // 活四
        OpenThree,  // 活三
        BlockedFour, // 冲四
        BlockedThree // 眠三
        // 可以添加更多的棋型...
    }
}

这个优化版的GomokuRules类引入了更高效的胜负判定算法和棋型检查功能。它使用方向数组来简化代码,并添加了检查特定棋型(如活三、活四)的方法。这些功能在实现AI和禁手规则时非常有用。

16.3.5 黑方禁手规则判断

前面我们已经实现了基本的禁手检查功能。现在,让我们使用改进的棋型检查方法来优化禁手规则判断:

csharp

public class ForbiddenMoveChecker
{
    private ChessBoard board;
    
    public ForbiddenMoveChecker(ChessBoard board)
    {
        this.board = board;
    }
    
    // 检查位置(x,y)是否是黑方的禁手点
    public bool IsForbiddenMove(int x, int y)
    {
        // 首先检查该位置是否为空
        if (board.GetChessPiece(x, y) != 0)
            return false;
            
        // 检查长连禁手
        if (CheckOverline(x, y))
            return true;
            
        // 检查三三禁手
        if (CheckDoubleThree(x, y))
            return true;
            
        // 检查四四禁手
        if (CheckDoubleFour(x, y))
            return true;
            
        return false;
    }
    
    // 检查长连禁手(超过五子相连)
    private bool CheckOverline(int x, int y)
    {
        // 方向数组:水平、垂直、右下对角线、左下对角线
        int[][] directions = new int[][]
        {
            new int[] {1, 0},
            new int[] {0, 1},
            new int[] {1, 1},
            new int[] {-1, 1}
        };
        
        // 临时放置一个黑子
        board.PlaceChessPiece(x, y, 1);
        
        bool result = false;
        
        foreach (int[] dir in directions)
        {
            int dx = dir[0];
            int dy = dir[1];
            
            int count = GomokuRules.CountConsecutivePieces(board, x, y, dx, dy);
            
            if (count > 5)
            {
                result = true;
                break;
            }
        }
        
        // 移除临时放置的黑子
        board.PlaceChessPiece(x, y, 0);
        
        return result;
    }
    
    // 检查三三禁手
    private bool CheckDoubleThree(int x, int y)
    {
        // 临时放置一个黑子
        board.PlaceChessPiece(x, y, 1);
        
        int openThreeCount = 0;
        
        // 检查水平方向
        if (GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.OpenThree))
            openThreeCount++;
            
        // 检查垂直方向
        if (GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.OpenThree))
            openThreeCount++;
            
        // 检查右下对角线
        if (GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.OpenThree))
            openThreeCount++;
            
        // 检查左下对角线
        if (GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.OpenThree))
            openThreeCount++;
            
        // 移除临时放置的黑子
        board.PlaceChessPiece(x, y, 0);
        
        // 如果有两个或更多活三,则是三三禁手
        return openThreeCount >= 2;
    }
    
    // 检查四四禁手
    private bool CheckDoubleFour(int x, int y)
    {
        // 临时放置一个黑子
        board.PlaceChessPiece(x, y, 1);
        
        int fourCount = 0;
        
        // 检查水平方向
        if (GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.OpenFour) ||
            GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.BlockedFour))
            fourCount++;
            
        // 检查垂直方向
        if (GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.OpenFour) ||
            GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.BlockedFour))
            fourCount++;
            
        // 检查右下对角线
        if (GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.OpenFour) ||
            GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.BlockedFour))
            fourCount++;
            
        // 检查左下对角线
        if (GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.OpenFour) ||
            GomokuRules.CheckPattern(board, x, y, 1, GomokuRules.PatternType.BlockedFour))
            fourCount++;
            
        // 移除临时放置的黑子
        board.PlaceChessPiece(x, y, 0);
        
        // 如果有两个或更多四,则是四四禁手
        return fourCount >= 2;
    }
}

这个优化版的ForbiddenMoveChecker类使用了GomokuRules类中的棋型检查方法,使代码更加简洁和可维护。它检查三种禁手:长连(超过五子相连)、三三禁手(形成两个或更多活三)和四四禁手(形成两个或更多四)。

16.3.6 游戏流程图与状态机

游戏流程可以用状态机来表示,这有助于我们理解和管理游戏的不同阶段。以下是五子棋游戏的状态机实现:

csharp

public class GameStateMachine : MonoBehaviour
{
    // 游戏状态
    public enum GameState
    {
        Initialize,
        PlayerTurn,
        CheckWin,
        GameOver,
        AITurn,
        CheckForbiddenMove
    }
    
    // 当前状态
    private GameState currentState;
    
    // 游戏组件
    public ChessBoardRenderer boardRenderer;
    private GameManager gameManager;
    private ForbiddenMoveChecker forbiddenMoveChecker;
    private GomokuAI ai;
    
    // UI组件
    public Text statusText;
    public GameObject gameOverPanel;
    public Text winnerText;
    
    // 当前选择的位置
    private Vector2Int currentPosition;
    
    // 初始化
    void Start()
    {
        // 创建游戏管理器
        gameManager = new GameManager();
        gameManager.SetBoard(boardRenderer.GetBoard());
        
        // 创建禁手检查器
        forbiddenMoveChecker = new ForbiddenMoveChecker(boardRenderer.GetBoard());
        
        // 创建AI
        ai = new GomokuAI(boardRenderer.GetBoard());
        
        // 设置初始状态
        currentState = GameState.Initialize;
        
        // 执行初始状态的操作
        EnterState(currentState);
    }
    
    // 更新
    void Update()
    {
        // 执行当前状态的逻辑
        switch (currentState)
        {
            case GameState.Initialize:
                UpdateInitializeState();
                break;
            case GameState.PlayerTurn:
                UpdatePlayerTurnState();
                break;
            case GameState.CheckWin:
                UpdateCheckWinState();
                break;
            case GameState.GameOver:
                UpdateGameOverState();
                break;
            case GameState.AITurn:
                UpdateAITurnState();
                break;
            case GameState.CheckForbiddenMove:
                UpdateCheckForbiddenMoveState();
                break;
        }
    }
    
    // 进入新状态
    private void EnterState(GameState newState)
    {
        currentState = newState;
        
        // 执行进入状态的操作
        switch (newState)
        {
            case GameState.Initialize:
                // 隐藏游戏结束面板
                gameOverPanel.SetActive(false);
                // 更新状态文本
                UpdateStatusText();
                // 转到玩家回合
                ChangeState(GameState.PlayerTurn);
                break;
                
            case GameState.PlayerTurn:
                // 更新状态文本
                UpdateStatusText();
                break;
                
            case GameState.CheckWin:
                // 检查胜负
                int result = GomokuRules.CheckGameOver(boardRenderer.GetBoard());
                
                if (result != 0)
                {
                    // 游戏结束,设置获胜者
                    gameManager.HandleGameOver(result);
                    ChangeState(GameState.GameOver);
                }
                else
                {
                    // 切换玩家
                    gameManager.SwitchPlayer();
                    
                    // 如果下一个是AI,则转到AI回合,否则回到玩家回合
                    if (gameManager.IsAITurn())
                        ChangeState(GameState.AITurn);
                    else
                        ChangeState(GameState.PlayerTurn);
                }
                break;
                
            case GameState.GameOver:
                // 显示游戏结束面板
                ShowGameOverPanel();
                break;
                
            case GameState.AITurn:
                // 更新状态文本
                UpdateStatusText();
                // 延迟执行AI移动
                StartCoroutine(PerformAIMove());
                break;
                
            case GameState.CheckForbiddenMove:
                // 检查是否是禁手位置
                if (forbiddenMoveChecker.IsForbiddenMove(currentPosition.x, currentPosition.y))
                {
                    // 显示禁手提示
                    statusText.text = "禁手位置,请选择其他位置";
                    // 回到玩家回合
                    ChangeState(GameState.PlayerTurn);
                }
                else
                {
                    // 放置棋子
                    boardRenderer.PlacePiece(currentPosition.x, currentPosition.y, gameManager.GetCurrentPlayer());
                    gameManager.HandlePlayerMove(currentPosition.x, currentPosition.y);
                    
                    // 检查胜负
                    ChangeState(GameState.CheckWin);
                }
                break;
        }
    }
    
    // 更新状态文本
    private void UpdateStatusText()
    {
        if (gameManager.GetCurrentPlayer() == 1)
        {
            statusText.text = "黑方回合";
        }
        else
        {
            statusText.text = "白方回合";
        }
    }
    
    // 显示游戏结束面板
    private void ShowGameOverPanel()
    {
        gameOverPanel.SetActive(true);
        
        int winner = gameManager.GetWinner();
        
        switch (winner)
        {
            case 1:
                winnerText.text = "黑方胜利!";
                break;
            case 2:
                winnerText.text = "白方胜利!";
                break;
            case 3:
                winnerText.text = "平局!";
                break;
        }
    }
    
    // 改变状态
    private void ChangeState(GameState newState)
    {
        EnterState(newState);
    }
    
    // 更新初始化状态
    private void UpdateInitializeState()
    {
        // 初始化状态只在Start中执行一次
    }
    
    // 更新玩家回合状态
    private void UpdatePlayerTurnState()
    {
        // 处理鼠标点击
        if (Input.GetMouseButtonDown(0))
        {
            // 将屏幕坐标转换为棋盘坐标
            currentPosition = boardRenderer.ScreenToBoard(Input.mousePosition);
            
            // 检查是否可以在该位置放置棋子
            if (boardRenderer.GetBoard().GetChessPiece(currentPosition.x, currentPosition.y) == 0)
            {
                // 如果是黑方,需要检查禁手
                if (gameManager.GetCurrentPlayer() == 1)
                {
                    ChangeState(GameState.CheckForbiddenMove);
                }
                else
                {
                    // 白方直接放置棋子
                    boardRenderer.PlacePiece(currentPosition.x, currentPosition.y, gameManager.GetCurrentPlayer());
                    gameManager.HandlePlayerMove(currentPosition.x, currentPosition.y);
                    
                    // 检查胜负
                    ChangeState(GameState.CheckWin);
                }
            }
        }
    }
    
    // 更新检查胜负状态
    private void UpdateCheckWinState()
    {
        // 检查胜负的逻辑在EnterState中已经实现
    }
    
    // 更新游戏结束状态
    private void UpdateGameOverState()
    {
        // 游戏结束状态不需要每帧更新
    }
    
    // 更新AI回合状态
    private void UpdateAITurnState()
    {
        // AI移动的逻辑在PerformAIMove协程中实现
    }
    
    // 更新检查禁手状态
    private void UpdateCheckForbiddenMoveState()
    {
        // 检查禁手的逻辑在EnterState中已经实现
    }
    
    // 执行AI移动
    private IEnumerator PerformAIMove()
    {
        // 延迟一段时间,使AI移动看起来更自然
        yield return new WaitForSeconds(0.5f);
        
        // 获取AI选择的位置
        Vector2Int aiMove = ai.GetNextMove(gameManager.GetCurrentPlayer());
        
        // 放置棋子
        boardRenderer.PlacePiece(aiMove.x, aiMove.y, gameManager.GetCurrentPlayer());
        gameManager.HandlePlayerMove(aiMove.x, aiMove.y);
        
        // 检查胜负
        ChangeState(GameState.CheckWin);
    }
    
    // 重新开始游戏
    public void RestartGame()
    {
        // 清除棋盘
        boardRenderer.ClearBoard();
        
        // 重置游戏管理器
        gameManager.ResetGame();
        
        // 返回初始状态
        ChangeState(GameState.Initialize);
    }
}

这个GameStateMachine类使用状态模式来管理游戏的不同阶段。它定义了六个状态:初始化、玩家回合、检查胜负、游戏结束、AI回合和检查禁手。每个状态都有自己的进入逻辑和更新逻辑,使游戏流程更加清晰和易于管理。

16.4 Unity游戏程序实现

16.4.1 前期准备工作

在开始实现游戏之前,我们需要做一些准备工作,包括创建项目、设置场景和导入资源。以下是一个简单的清单:

  1. 创建一个新的Unity项目(使用Unity 2021.3.8f1c1版本)。

  2. 创建以下文件夹结构:

    • Scripts(存放脚本文件)
    • Prefabs(存放预制体)
    • Sprites(存放贴图)
    • Scenes(存放场景)
    • Materials(存放材质)
  3. 导入或创建所需的素材:

    • 棋盘背景贴图
    • 黑棋和白棋的贴图
    • UI元素(按钮、面板等)
  4. 创建一个新场景,命名为"GomokuGame"。

接下来,我们将实现游戏的各个组件。

16.4.2 游戏场景搭建

首先,我们需要搭建游戏的基本场景结构:

  1. 设置摄像机:

    csharp

    // 设置主相机
    Camera.main.transform.position = new Vector3(0, 0, -10);
    Camera.main.orthographic = true;
    Camera.main.orthographicSize = 10;
    

  2. 创建棋盘对象:

    csharp

    // 创建棋盘对象
    GameObject boardObject = new GameObject("ChessBoard");
    boardObject.AddComponent<ChessBoardRenderer>();
    

  3. 创建UI元素:

    csharp

    // 创建UI画布
    GameObject canvasObject = new GameObject("Canvas");
    Canvas canvas = canvasObject.AddComponent<Canvas>();
    canvas.renderMode = RenderMode.ScreenSpaceOverlay;
    canvasObject.AddComponent<CanvasScaler>();
    canvasObject.AddComponent<GraphicRaycaster>();
    
    // 创建状态文本
    GameObject statusTextObject = new GameObject("StatusText");
    statusTextObject.transform.SetParent(canvasObject.transform, false);
    Text statusText = statusTextObject.AddComponent<Text>();
    statusText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
    statusText.fontSize = 24;
    statusText.alignment = TextAnchor.UpperCenter;
    RectTransform statusTextRect = statusText.GetComponent<RectTransform>();
    statusTextRect.anchorMin = new Vector2(0, 1);
    statusTextRect.anchorMax = new Vector2(1, 1);
    statusTextRect.pivot = new Vector2(0.5f, 1);
    statusTextRect.offsetMin = new Vector2(0, -50);
    statusTextRect.offsetMax = new Vector2(0, 0);
    
    // 创建游戏结束面板
    GameObject gameOverPanelObject = new GameObject("GameOverPanel");
    gameOverPanelObject.transform.SetParent(canvasObject.transform, false);
    Image gameOverPanel = gameOverPanelObject.AddComponent<Image>();
    gameOverPanel.color = new Color(0, 0, 0, 0.8f);
    RectTransform gameOverPanelRect = gameOverPanel.GetComponent<RectTransform>();
    gameOverPanelRect.anchorMin = new Vector2(0.3f, 0.3f);
    gameOverPanelRect.anchorMax = new Vector2(0.7f, 0.7f);
    gameOverPanelRect.offsetMin = Vector2.zero;
    gameOverPanelRect.offsetMax = Vector2.zero;
    
    // 创建获胜者文本
    GameObject winnerTextObject = new GameObject("WinnerText");
    winnerTextObject.transform.SetParent(gameOverPanelObject.transform, false);
    Text winnerText = winnerTextObject.AddComponent<Text>();
    winnerText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
    winnerText.fontSize = 32;
    winnerText.alignment = TextAnchor.MiddleCenter;
    winnerText.color = Color.white;
    RectTransform winnerTextRect = winnerText.GetComponent<RectTransform>();
    winnerTextRect.anchorMin = new Vector2(0, 0.6f);
    winnerTextRect.anchorMax = new Vector2(1, 0.8f);
    winnerTextRect.offsetMin = Vector2.zero;
    winnerTextRect.offsetMax = Vector2.zero;
    
    // 创建重新开始按钮
    GameObject restartButtonObject = new GameObject("RestartButton");
    restartButtonObject.transform.SetParent(gameOverPanelObject.transform, false);
    Image restartButtonImage = restartButtonObject.AddComponent<Image>();
    restartButtonImage.color = new Color(0.2f, 0.6f, 0.8f);
    Button restartButton = restartButtonObject.AddComponent<Button>();
    restartButton.targetGraphic = restartButtonImage;
    RectTransform restartButtonRect = restartButton.GetComponent<RectTransform>();
    restartButtonRect.anchorMin = new Vector2(0.25f, 0.2f);
    restartButtonRect.anchorMax = new Vector2(0.75f, 0.4f);
    restartButtonRect.offsetMin = Vector2.zero;
    restartButtonRect.offsetMax = Vector2.zero;
    
    // 创建按钮文本
    GameObject restartTextObject = new GameObject("RestartText");
    restartTextObject.transform.SetParent(restartButtonObject.transform, false);
    Text restartText = restartTextObject.AddComponent<Text>();
    restartText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
    restartText.fontSize = 24;
    restartText.alignment = TextAnchor.MiddleCenter;
    restartText.color = Color.white;
    restartText.text = "重新开始";
    RectTransform restartTextRect = restartText.GetComponent<RectTransform>();
    restartTextRect.anchorMin = Vector2.zero;
    restartTextRect.anchorMax = Vector2.one;
    restartTextRect.offsetMin = Vector2.zero;
    restartTextRect.offsetMax = Vector2.zero;
    
    // 初始隐藏游戏结束面板
    gameOverPanelObject.SetActive(false);
    

  4. 添加游戏管理器:

    csharp

    // 创建游戏管理器对象
    GameObject gameManagerObject = new GameObject("GameManager");
    GameStateMachine gameStateMachine = gameManagerObject.AddComponent<GameStateMachine>();
    
    // 设置引用
    gameStateMachine.boardRenderer = boardObject.GetComponent<ChessBoardRenderer>();
    gameStateMachine.statusText = statusText;
    gameStateMachine.gameOverPanel = gameOverPanelObject;
    gameStateMachine.winnerText = winnerText;
    
    // 设置按钮事件
    restartButton.onClick.AddListener(gameStateMachine.RestartGame);
    

这段代码创建了游戏的基本场景,包括棋盘、UI元素和游戏管理器。UI元素包括状态文本、游戏结束面板和重新开始按钮。

16.4.3 落子操作与交互实现

落子操作是游戏的核心交互。我们需要实现以下功能:

  1. 检测鼠标点击
  2. 将鼠标坐标转换为棋盘坐标
  3. 验证移动是否合法
  4. 放置棋子
  5. 处理游戏逻辑

以下是ChessBoardRenderer类中落子相关的方法:

csharp

public class ChessBoardRenderer : MonoBehaviour
{
    // 其他代码保持不变...
    
    // 处理鼠标点击
    public Vector2Int HandleMouseClick(Vector3 mousePosition)
    {
        // 将屏幕坐标转换为棋盘坐标
        Vector2Int boardPos = ScreenToBoard(mousePosition);
        
        // 检查坐标是否有效
        if (boardPos.x >= 0 && boardPos.x < ChessBoard.BOARD_SIZE &&
            boardPos.y >= 0 && boardPos.y < ChessBoard.BOARD_SIZE)
        {
            return boardPos;
        }
        
        // 返回无效坐标
        return new Vector2Int(-1, -1);
    }
    
    // 放置棋子
    public bool PlacePiece(int x, int y, int pieceType)
    {
        // 检查位置是否为空
        if (chessBoard.GetChessPiece(x, y) != 0)
            return false;
            
        // 更新棋盘状态
        chessBoard.PlaceChessPiece(x, y, pieceType);
        
        // 创建棋子对象
        CreatePieceObject(x, y, pieceType);
        
        return true;
    }
    
    // 创建棋子对象
    private void CreatePieceObject(int x, int y, int pieceType)
    {
        // 选择合适的预制体
        GameObject prefab = pieceType == 1 ? blackPiecePrefab : whitePiecePrefab;
        
        // 实例化棋子
        GameObject piece = Instantiate(prefab, transform);
        
        // 计算位置
        float posX = x - (ChessBoard.BOARD_SIZE - 1) / 2f;
        float posY = y - (ChessBoard.BOARD_SIZE - 1) / 2f;
        
        piece.transform.localPosition = new Vector3(posX, posY, -0.05f);
        
        // 添加动画效果
        StartCoroutine(AnimatePiecePlacement(piece));
    }
    
    // 棋子放置动画
    private IEnumerator AnimatePiecePlacement(GameObject piece)
    {
        // 初始缩放为0
        piece.transform.localScale = Vector3.zero;
        
        // 动画时长
        float duration = 0.2f;
        float elapsed = 0;
        
        // 执行动画
        while (elapsed < duration)
        {
            // 计算动画进度
            elapsed += Time.deltaTime;
            float t = elapsed / duration;
            
            // 使用弹性函数
            float scale = Mathf.Sin(t * Mathf.PI * 0.5f);
            piece.transform.localScale = new Vector3(scale, scale, scale);
            
            yield return null;
        }
        
        // 确保最终大小正确
        piece.transform.localScale = Vector3.one;
    }
}

这段代码实现了棋子的放置逻辑和动画效果。HandleMouseClick方法将屏幕坐标转换为棋盘坐标,PlacePiece方法在指定位置放置棋子,而AnimatePiecePlacement协程则添加了棋子出现时的动画效果。

16.4.4 玩家交替与权限控制

在五子棋游戏中,玩家轮流落子,每次只能放一颗棋子。我们需要实现玩家交替和权限控制的逻辑:

csharp

public class GameManager
{
    // 当前玩家(1:黑方,2:白方)
    private int currentPlayer = 1;
    
    // 游戏是否结束
    private bool gameIsOver = false;
    
    // 赢家(0:未结束,1:黑方,2:白方,3:平局)
    private int winner = 0;
    
    // 棋盘引用
    private ChessBoard board;
    
    // 游戏模式(0:人人对战,1:人机对战)
    private int gameMode = 0;
    
    // 玩家颜色(在人机模式中,1:人执黑,2:人执白)
    private int playerColor = 1;
    
    // 构造函数
    public GameManager()
    {
        board = new ChessBoard();
    }
    
    // 设置游戏模式
    public void SetGameMode(int mode, int color = 1)
    {
        gameMode = mode;
        playerColor = color;
    }
    
    // 检查当前是否是AI回合
    public bool IsAITurn()
    {
        return gameMode == 1 && currentPlayer != playerColor;
    }
    
    // 检查当前是否是玩家回合
    public bool IsPlayerTurn()
    {
        return gameMode == 0 || currentPlayer == playerColor;
    }
    
    // 切换当前玩家
    public void SwitchPlayer()
    {
        currentPlayer = currentPlayer == 1 ? 2 : 1;
    }
    
    // 处理玩家落子
    public bool HandlePlayerMove(int x, int y)
    {
        // 如果游戏已结束或不是玩家回合,不允许落子
        if (gameIsOver || !IsPlayerTurn())
            return false;
            
        // 检查位置是否有效
        if (board.GetChessPiece(x, y) != 0)
            return false;
            
        // 更新棋盘状态
        board.PlaceChessPiece(x, y, currentPlayer);
        
        return true;
    }
    
    // 处理游戏结束
    public void HandleGameOver(int result)
    {
        gameIsOver = true;
        winner = result;
    }
    
    // 重置游戏
    public void ResetGame()
    {
        currentPlayer = 1;
        gameIsOver = false;
        winner = 0;
        board.ClearBoard();
    }
    
    // 获取当前玩家
    public int GetCurrentPlayer()
    {
        return currentPlayer;
    }
    
    // 游戏是否结束
    public bool IsGameOver()
    {
        return gameIsOver;
    }
    
    // 获取赢家
    public int GetWinner()
    {
        return winner;
    }
}

这个扩展的GameManager类增加了游戏模式和玩家颜色的概念,支持人人对战和人机对战两种模式。IsAITurnIsPlayerTurn方法根据当前游戏模式和玩家颜色判断当前是否是AI或玩家的回合。

16.4.5 棋盘状态更新与显示

每当棋子放置在棋盘上,我们需要更新棋盘状态并重新渲染界面。以下是实现这一功能的代码:

csharp

public class ChessBoardRenderer : MonoBehaviour
{
    // 其他代码保持不变...
    
    // 更新棋盘显示
    public void UpdateBoard()
    {
        // 清除所有棋子对象
        foreach (Transform child in transform)
        {
            if (child.name.StartsWith("Piece_"))
            {
                Destroy(child.gameObject);
            }
        }
        
        // 根据棋盘状态重新创建棋子对象
        for (int x = 0; x < ChessBoard.BOARD_SIZE; x++)
        {
            for (int y = 0; y < ChessBoard.BOARD_SIZE; y++)
            {
                int pieceType = chessBoard.GetChessPiece(x, y);
                if (pieceType != 0)
                {
                    // 创建棋子对象(不带动画)
                    CreatePieceObjectWithoutAnimation(x, y, pieceType);
                }
            }
        }
    }
    
    // 创建棋子对象(不带动画)
    private void CreatePieceObjectWithoutAnimation(int x, int y, int pieceType)
    {
        // 选择合适的预制体
        GameObject prefab = pieceType == 1 ? blackPiecePrefab : whitePiecePrefab;
        
        // 实例化棋子
        GameObject piece = Instantiate(prefab, transform);
        piece.name = $"Piece_{x}_{y}";
        
        // 计算位置
        float posX = x - (ChessBoard.BOARD_SIZE - 1) / 2f;
        float posY = y - (ChessBoard.BOARD_SIZE - 1) / 2f;
        
        piece.transform.localPosition = new Vector3(posX, posY, -0.05f);
    }
    
    // 高亮显示最后一步
    public void HighlightLastMove(int x, int y)
    {
        // 创建高亮标记
        GameObject highlight = new GameObject("LastMoveHighlight");
        highlight.transform.parent = transform;
        
        // 添加精灵渲染器
        SpriteRenderer renderer = highlight.AddComponent<SpriteRenderer>();
        renderer.sprite = highlightSprite;
        renderer.color = new Color(1, 1, 0, 0.5f); // 半透明黄色
        
        // 设置位置
        float posX = x - (ChessBoard.BOARD_SIZE - 1) / 2f;
        float posY = y - (ChessBoard.BOARD_SIZE - 1) / 2f;
        
        highlight.transform.localPosition = new Vector3(posX, posY, -0.04f);
        
        // 设置缩放
        highlight.transform.localScale = new Vector3(0.8f, 0.8f, 1);
        
        // 添加闪烁动画
        StartCoroutine(BlinkHighlight(highlight));
    }
    
    // 闪烁动画
    private IEnumerator BlinkHighlight(GameObject highlight)
    {
        SpriteRenderer renderer = highlight.GetComponent<SpriteRenderer>();
        
        // 闪烁次数
        int blinkCount = 3;
        
        for (int i = 0; i < blinkCount; i++)
        {
            // 淡出
            for (float alpha = 0.5f; alpha >= 0; alpha -= 0.05f)
            {
                renderer.color = new Color(1, 1, 0, alpha);
                yield return new WaitForSeconds(0.05f);
            }
            
            // 淡入
            for (float alpha = 0; alpha <= 0.5f; alpha += 0.05f)
            {
                renderer.color = new Color(1, 1, 0, alpha);
                yield return new WaitForSeconds(0.05f);
            }
        }
        
        // 销毁高亮标记
        Destroy(highlight);
    }
}

这段代码增加了更新棋盘显示和高亮最后一步的功能。UpdateBoard方法根据当前棋盘状态重新创建所有棋子对象,而HighlightLastMove方法则在指定位置创建一个高亮标记,并添加闪烁动画。

16.4.6 胜负判断与结果显示

当一方连成五子或棋盘已满时,游戏结束。我们需要判断胜负并显示结果:

csharp

public class GameStateMachine : MonoBehaviour
{
    // 其他代码保持不变...
    
    // 检查游戏是否结束
    private void CheckGameOver()
    {
        // 获取当前棋盘状态
        ChessBoard board = boardRenderer.GetBoard();
        
        // 检查是否有一方获胜
        int result = GomokuRules.CheckGameOver(board);
        
        if (result != 0)
        {
            // 游戏结束
            gameManager.HandleGameOver(result);
            ChangeState(GameState.GameOver);
        }
    }
    
    // 显示游戏结束面板
    private void ShowGameOverPanel()
    {
        gameOverPanel.SetActive(true);
        
        int winner = gameManager.GetWinner();
        
        switch (winner)
        {
            case 1:
                winnerText.text = "黑方胜利!";
                // 播放胜利音效
                AudioManager.Instance.PlaySound(AudioManager.SoundType.Victory);
                break;
            case 2:
                winnerText.text = "白方胜利!";
                // 播放胜利音效
                AudioManager.Instance.PlaySound(AudioManager.SoundType.Victory);
                break;
            case 3:
                winnerText.text = "平局!";
                // 播放平局音效
                AudioManager.Instance.PlaySound(AudioManager.SoundType.Draw);
                break;
        }
        
        // 添加胜利动画
        StartCoroutine(PlayVictoryAnimation());
    }
    
    // 胜利动画
    private IEnumerator PlayVictoryAnimation()
    {
        // 获取获胜的连子位置
        List<Vector2Int> winningLine = GomokuRules.GetWinningLine(boardRenderer.GetBoard());
        
        if (winningLine.Count >= 5)
        {
            // 高亮显示获胜的连子
            foreach (Vector2Int pos in winningLine)
            {
                // 创建高亮效果
                GameObject highlight = new GameObject("WinHighlight");
                highlight.transform.SetParent(boardRenderer.transform);
                
                SpriteRenderer renderer = highlight.AddComponent<SpriteRenderer>();
                renderer.sprite = winHighlightSprite;
                renderer.color = new Color(1, 0.5f, 0, 0.7f); // 橙色
                
                // 设置位置
                float posX = pos.x - (ChessBoard.BOARD_SIZE - 1) / 2f;
                float posY = pos.y - (ChessBoard.BOARD_SIZE - 1) / 2f;
                
                highlight.transform.localPosition = new Vector3(posX, posY, -0.03f);
                
                // 缩放从0到1的动画
                highlight.transform.localScale = Vector3.zero;
                
                for (float scale = 0; scale <= 1; scale += 0.1f)
                {
                    highlight.transform.localScale = new Vector3(scale, scale, 1);
                    yield return new WaitForSeconds(0.02f);
                }
            }
            
            // 等待一段时间
            yield return new WaitForSeconds(1.0f);
        }
    }
}

// 扩展GomokuRules类,添加获取获胜连子的方法
public static class GomokuRules
{
    // 其他代码保持不变...
    
    // 获取获胜的连子位置
    public static List<Vector2Int> GetWinningLine(ChessBoard board)
    {
        List<Vector2Int> winningLine = new List<Vector2Int>();
        
        // 检查是否有一方获胜
        for (int x = 0; x < ChessBoard.BOARD_SIZE; x++)
        {
            for (int y = 0; y < ChessBoard.BOARD_SIZE; y++)
            {
                int piece = board.GetChessPiece(x, y);
                if (piece == 0)
                    continue;
                    
                // 检查各个方向
                foreach (int[] dir in directions)
                {
                    int dx = dir[0];
                    int dy = dir[1];
                    
                    // 检查是否形成五连
                    List<Vector2Int> line = GetConsecutiveLine(board, x, y, dx, dy);
                    
                    if (line.Count >= 5)
                    {
                        return line;
                    }
                }
            }
        }
        
        return winningLine;
    }
    
    // 获取连续的同色棋子位置
    private static List<Vector2Int> GetConsecutiveLine(ChessBoard board, int x, int y, int dx, int dy)
    {
        int piece = board.GetChessPiece(x, y);
        if (piece == 0)
            return new List<Vector2Int>();
            
        List<Vector2Int> line = new List<Vector2Int> { new Vector2Int(x, y) };
        
        // 向一个方向检查
        for (int i = 1; i < 5; i++)
        {
            int nx = x + dx * i;
            int ny = y + dy * i;
            
            if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                break;
                
            if (board.GetChessPiece(nx, ny) != piece)
                break;
                
            line.Add(new Vector2Int(nx, ny));
        }
        
        // 向反方向检查
        for (int i = 1; i < 5; i++)
        {
            int nx = x - dx * i;
            int ny = y - dy * i;
            
            if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                break;
                
            if (board.GetChessPiece(nx, ny) != piece)
                break;
                
            line.Add(new Vector2Int(nx, ny));
        }
        
        return line;
    }
}

这段代码添加了游戏结束的检查和结果显示。CheckGameOver方法检查是否有一方获胜,ShowGameOverPanel方法显示游戏结束面板和获胜者信息,而PlayVictoryAnimation协程则添加了获胜连子的高亮动画效果。我们还扩展了GomokuRules类,添加了获取获胜连子位置的方法。

16.4.7 禁手规则实现

在标准五子棋规则中,黑方有禁手规则。我们需要在黑方落子前检查是否违反禁手规则:

csharp

public class GameController : MonoBehaviour
{
    // 其他代码保持不变...
    
    // 处理玩家输入
    void Update()
    {
        // 如果游戏已结束,不处理输入
        if (gameManager.IsGameOver())
            return;
            
        // 如果不是玩家回合,不处理输入
        if (!gameManager.IsPlayerTurn())
            return;
            
        // 处理鼠标点击
        if (Input.GetMouseButtonDown(0))
        {
            // 获取棋盘坐标
            Vector2Int boardPos = boardRenderer.HandleMouseClick(Input.mousePosition);
            
            // 检查坐标是否有效
            if (boardPos.x >= 0 && boardPos.x < ChessBoard.BOARD_SIZE &&
                boardPos.y >= 0 && boardPos.y < ChessBoard.BOARD_SIZE)
            {
                // 检查是否是空位
                if (boardRenderer.GetBoard().GetChessPiece(boardPos.x, boardPos.y) == 0)
                {
                    // 如果是黑方,检查禁手
                    if (gameManager.GetCurrentPlayer() == 1 && 
                        forbiddenMoveChecker.IsForbiddenMove(boardPos.x, boardPos.y))
                    {
                        // 显示禁手提示
                        ShowForbiddenMoveMessage();
                        
                        // 播放禁手音效
                        AudioManager.Instance.PlaySound(AudioManager.SoundType.Forbidden);
                        
                        return;
                    }
                    
                    // 尝试放置棋子
                    if (boardRenderer.PlacePiece(boardPos.x, boardPos.y, gameManager.GetCurrentPlayer()))
                    {
                        // 播放落子音效
                        AudioManager.Instance.PlaySound(AudioManager.SoundType.PlacePiece);
                        
                        // 高亮显示最后一步
                        boardRenderer.HighlightLastMove(boardPos.x, boardPos.y);
                        
                        // 处理玩家移动
                        gameManager.HandlePlayerMove(boardPos.x, boardPos.y);
                        
                        // 检查游戏是否结束
                        CheckGameOver();
                        
                        if (!gameManager.IsGameOver())
                        {
                            // 切换玩家
                            gameManager.SwitchPlayer();
                            
                            // 更新状态文本
                            UpdateStatusText();
                            
                            // 如果下一个是AI,执行AI移动
                            if (gameManager.IsAITurn())
                            {
                                StartCoroutine(PerformAIMove());
                            }
                        }
                    }
                }
            }
        }
    }
    
    // 显示禁手提示
    private void ShowForbiddenMoveMessage()
    {
        // 创建提示文本
        GameObject messageObject = new GameObject("ForbiddenMessage");
        messageObject.transform.SetParent(transform);
        
        Text message = messageObject.AddComponent<Text>();
        message.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        message.fontSize = 28;
        message.alignment = TextAnchor.MiddleCenter;
        message.color = Color.red;
        message.text = "禁手位置!请选择其他位置";
        
        RectTransform rectTransform = message.GetComponent<RectTransform>();
        rectTransform.anchorMin = new Vector2(0, 1);
        rectTransform.anchorMax = new Vector2(1, 1);
        rectTransform.pivot = new Vector2(0.5f, 1);
        rectTransform.offsetMin = new Vector2(0, -100);
        rectTransform.offsetMax = new Vector2(0, -50);
        
        // 淡出动画
        StartCoroutine(FadeOutText(message));
    }
    
    // 文本淡出动画
    private IEnumerator FadeOutText(Text text)
    {
        // 等待一段时间
        yield return new WaitForSeconds(1.5f);
        
        // 淡出动画
        for (float alpha = 1; alpha >= 0; alpha -= 0.05f)
        {
            text.color = new Color(text.color.r, text.color.g, text.color.b, alpha);
            yield return new WaitForSeconds(0.05f);
        }
        
        // 销毁对象
        Destroy(text.gameObject);
    }
}

这段代码实现了禁手规则的检查和提示显示。在黑方落子前,它使用forbiddenMoveChecker.IsForbiddenMove方法检查是否违反禁手规则。如果违反,显示一个提示文本并播放提示音效,然后在一段时间后淡出。

16.4.8 游戏重置与重新开始

游戏结束后,玩家可能想要重新开始。我们需要实现重置游戏的功能:

csharp

public class GameManager : MonoBehaviour
{
    // 其他代码保持不变...
    
    // 重新开始游戏
    public void RestartGame()
    {
        // 隐藏游戏结束面板
        gameOverPanel.SetActive(false);
        
        // 清除棋盘
        boardRenderer.ClearBoard();
        
        // 重置游戏状态
        gameManager.ResetGame();
        
        // 更新状态文本
        UpdateStatusText();
        
        // 播放游戏开始音效
        AudioManager.Instance.PlaySound(AudioManager.SoundType.GameStart);
    }
}

public class ChessBoardRenderer : MonoBehaviour
{
    // 其他代码保持不变...
    
    // 清除棋盘
    public void ClearBoard()
    {
        // 清除所有棋子对象
        foreach (Transform child in transform)
        {
            // 只销毁棋子和高亮对象
            if (child.name.StartsWith("Piece_") || child.name.Contains("Highlight"))
            {
                Destroy(child.gameObject);
            }
        }
        
        // 重置棋盘状态
        chessBoard.ClearBoard();
    }
}

这段代码实现了游戏的重置功能。RestartGame方法隐藏游戏结束面板,清除棋盘上的所有棋子,并重置游戏状态。ClearBoard方法销毁所有棋子对象并重置棋盘状态。

16.5 AI对战系统实现

为了实现与电脑对战的功能,我们需要设计一个AI系统。在五子棋中,AI通常使用极小极大算法或其变体(如Alpha-Beta剪枝)来决定下一步的最佳位置。

16.5.1 评估函数设计

AI需要一个评估函数来判断棋盘状态的好坏。以下是一个简单的评估函数实现:

csharp

public class GomokuEvaluator
{
    // 棋型得分
    private static readonly int FIVE_SCORE = 100000;         // 五连
    private static readonly int OPEN_FOUR_SCORE = 10000;     // 活四
    private static readonly int BLOCKED_FOUR_SCORE = 1000;   // 冲四
    private static readonly int OPEN_THREE_SCORE = 1000;     // 活三
    private static readonly int BLOCKED_THREE_SCORE = 100;   // 眠三
    private static readonly int OPEN_TWO_SCORE = 100;        // 活二
    private static readonly int BLOCKED_TWO_SCORE = 10;      // 眠二
    
    // 评估棋盘状态(对于指定玩家)
    public static int EvaluateBoard(ChessBoard board, int player)
    {
        int opponentPlayer = player == 1 ? 2 : 1;
        
        // 检查是否已经胜利或失败
        int winner = GomokuRules.CheckGameOver(board);
        
        if (winner == player)
            return int.MaxValue;
        if (winner == opponentPlayer)
            return int.MinValue;
        
        // 计算玩家和对手的棋型分数
        int playerScore = CalculatePatternScore(board, player);
        int opponentScore = CalculatePatternScore(board, opponentPlayer);
        
        // 返回最终评分
        return playerScore - opponentScore;
    }
    
    // 计算指定玩家的棋型分数
    private static int CalculatePatternScore(ChessBoard board, int player)
    {
        int totalScore = 0;
        
        // 检查所有可能的棋型
        for (int x = 0; x < ChessBoard.BOARD_SIZE; x++)
        {
            for (int y = 0; y < ChessBoard.BOARD_SIZE; y++)
            {
                if (board.GetChessPiece(x, y) != player)
                    continue;
                    
                // 检查四个方向(水平、垂直、两条对角线)
                int[][] directions = new int[][]
                {
                    new int[] {1, 0},  // 水平
                    new int[] {0, 1},  // 垂直
                    new int[] {1, 1},  // 右下对角线
                    new int[] {-1, 1}  // 左下对角线
                };
                
                foreach (int[] dir in directions)
                {
                    int dx = dir[0];
                    int dy = dir[1];
                    
                    // 计算连子数和两端的状态
                    PatternInfo pattern = GetPatternInfo(board, x, y, dx, dy, player);
                    
                    // 根据棋型计算分数
                    int patternScore = CalculatePatternScore(pattern);
                    
                    totalScore += patternScore;
                }
            }
        }
        
        return totalScore;
    }
    
    // 计算指定棋型的分数
    private static int CalculatePatternScore(PatternInfo pattern)
    {
        if (pattern.Count >= 5)
            return FIVE_SCORE;
            
        if (pattern.Count == 4)
        {
            if (pattern.OpenEnds == 2)
                return OPEN_FOUR_SCORE;
            if (pattern.OpenEnds == 1)
                return BLOCKED_FOUR_SCORE;
        }
        
        if (pattern.Count == 3)
        {
            if (pattern.OpenEnds == 2)
                return OPEN_THREE_SCORE;
            if (pattern.OpenEnds == 1)
                return BLOCKED_THREE_SCORE;
        }
        
        if (pattern.Count == 2)
        {
            if (pattern.OpenEnds == 2)
                return OPEN_TWO_SCORE;
            if (pattern.OpenEnds == 1)
                return BLOCKED_TWO_SCORE;
        }
        
        return 0;
    }
    
    // 获取棋型信息(连子数和两端状态)
    private static PatternInfo GetPatternInfo(ChessBoard board, int x, int y, int dx, int dy, int player)
    {
        int count = 1;
        int openEnds = 0;
        
        // 向一个方向
        int steps = 1;
        while (true)
        {
            int nx = x + dx * steps;
            int ny = y + dy * steps;
            
            if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                break;
                
            if (board.GetChessPiece(nx, ny) != player)
                break;
                
            count++;
            steps++;
        }
        
        // 检查该方向的端点是否开放
        int endX = x + dx * steps;
        int endY = y + dy * steps;
        
        if (endX >= 0 && endX < ChessBoard.BOARD_SIZE && endY >= 0 && endY < ChessBoard.BOARD_SIZE &&
            board.GetChessPiece(endX, endY) == 0)
        {
            openEnds++;
        }
        
        // 向另一个方向
        steps = 1;
        while (true)
        {
            int nx = x - dx * steps;
            int ny = y - dy * steps;
            
            if (nx < 0 || nx >= ChessBoard.BOARD_SIZE || ny < 0 || ny >= ChessBoard.BOARD_SIZE)
                break;
                
            if (board.GetChessPiece(nx, ny) != player)
                break;
                
            count++;
            steps++;
        }
        
        // 检查该方向的端点是否开放
        endX = x - dx * steps;
        endY = y - dy * steps;
        
        if (endX >= 0 && endX < ChessBoard.BOARD_SIZE && endY >= 0 && endY < ChessBoard.BOARD_SIZE &&
            board.GetChessPiece(endX, endY) == 0)
        {
            openEnds++;
        }
        
        return new PatternInfo { Count = count, OpenEnds = openEnds };
    }
    
    // 棋型信息结构
    private struct PatternInfo
    {
        public int Count;      // 连子数
        public int OpenEnds;   // 开放端数(0、1或2)
    }
}

这个GomokuEvaluator类实现了五子棋的评估函数。它检查棋盘上的所有棋子,计算各种棋型(如五连、活四、冲四等)的数量,并根据棋型的重要性分配不同的分数。最终返回的评分表示棋盘状态对于指定玩家的有利程度。

16.5.2 极小极大算法与Alpha-Beta剪枝

现在我们实现极小极大算法和Alpha-Beta剪枝来提高AI的效率:

csharp

public class GomokuAI
{
    private ChessBoard board;
    private int maxDepth;
    private Random random;
    
    public GomokuAI(ChessBoard board, int maxDepth = 3)
    {
        this.board = board;
        this.maxDepth = maxDepth;
        this.random = new Random();
    }
    
    // 获取AI的下一步移动
    public Vector2Int GetNextMove(int aiPlayer)
    {
        // 如果是第一步,下在天元
        if (IsBoardEmpty())
        {
            return new Vector2Int(7, 7);
        }
        
        // 使用极小极大算法选择最佳移动
        Vector2Int bestMove = Vector2Int.zero;
        int bestScore = int.MinValue;
        
        // 生成可能的移动
        List<Vector2Int> possibleMoves = GenerateMoves();
        
        // 打乱顺序,增加随机性
        Shuffle(possibleMoves);
        
        foreach (Vector2Int move in possibleMoves)
        {
            // 尝试移动
            board.PlaceChessPiece(move.x, move.y, aiPlayer);
            
            // 评估移动
            int score = MinMax(maxDepth - 1, false, aiPlayer, int.MinValue, int.MaxValue);
            
            // 撤销移动
            board.PlaceChessPiece(move.x, move.y, 0);
            
            if (score > bestScore)
            {
                bestScore = score;
                bestMove = move;
            }
        }
        
        return bestMove;
    }
    
    // 极小极大算法
    private int MinMax(int depth, bool isMaximizing, int aiPlayer, int alpha, int beta)
    {
        int opponentPlayer = aiPlayer == 1 ? 2 : 1;
        
        // 检查是否已经胜利或失败或达到最大深度
        int winner = GomokuRules.CheckGameOver(board);
        
        if (winner == aiPlayer)
            return 10000 + depth; // 加上深度,使得更早的胜利得分更高
        
        if (winner == opponentPlayer)
            return -10000 - depth;
            
        if (winner == 3) // 平局
            return 0;
            
        if (depth == 0)
            return GomokuEvaluator.EvaluateBoard(board, aiPlayer);
            
        // 生成可能的移动
        List<Vector2Int> possibleMoves = GenerateMoves();
        
        // 没有可用移动,返回当前评分
        if (possibleMoves.Count == 0)
            return GomokuEvaluator.EvaluateBoard(board, aiPlayer);
            
        if (isMaximizing)
        {
            int maxScore = int.MinValue;
            
            foreach (Vector2Int move in possibleMoves)
            {
                // 尝试移动
                board.PlaceChessPiece(move.x, move.y, aiPlayer);
                
                // 递归评估
                int score = MinMax(depth - 1, false, aiPlayer, alpha, beta);
                
                // 撤销移动
                board.PlaceChessPiece(move.x, move.y, 0);
                
                maxScore = Math.Max(maxScore, score);
                alpha = Math.Max(alpha, score);
                
                // Alpha-Beta剪枝
                if (beta <= alpha)
                    break;
            }
            
            return maxScore;
        }
        else
        {
            int minScore = int.MaxValue;
            
            foreach (Vector2Int move in possibleMoves)
            {
                // 尝试移动
                board.PlaceChessPiece(move.x, move.y, opponentPlayer);
                
                // 递归评估
                int score = MinMax(depth - 1, true, aiPlayer, alpha, beta);
                
                // 撤销移动
                board.PlaceChessPiece(move.x, move.y, 0);
                
                minScore = Math.Min(minScore, score);
                beta = Math.Min(beta, score);
                
                // Alpha-Beta剪枝
                if (beta <= alpha)
                    break;
            }
            
            return minScore;
        }
    }
    
    // 检查棋盘是否为空
    private bool IsBoardEmpty()
    {
        for (int x = 0; x < ChessBoard.BOARD_SIZE; x++)
        {
            for (int y = 0; y < ChessBoard.BOARD_SIZE; y++)
            {
                if (board.GetChessPiece(x, y) != 0)
                    return false;
            }
        }
        
        return true;
    }
    
    // 生成可能的移动
    private List<Vector2Int> GenerateMoves()
    {
        List<Vector2Int> moves = new List<Vector2Int>();
        
        // 获取当前所有棋子的位置
        List<Vector2Int> pieces = new List<Vector2Int>();
        
        for (int x = 0; x < ChessBoard.BOARD_SIZE; x++)
        {
            for (int y = 0; y < ChessBoard.BOARD_SIZE; y++)
            {
                if (board.GetChessPiece(x, y) != 0)
                {
                    pieces.Add(new Vector2Int(x, y));
                }
            }
        }
        
        // 如果棋盘为空,返回中心位置
        if (pieces.Count == 0)
        {
            moves.Add(new Vector2Int(7, 7));
            return moves;
        }
        
        // 只考虑已有棋子周围的空位
        HashSet<Vector2Int> consideredMoves = new HashSet<Vector2Int>();
        
        foreach (Vector2Int piece in pieces)
        {
            // 检查周围3x3范围内的空位
            for (int dx = -2; dx <= 2; dx++)
            {
                for (int dy = -2; dy <= 2; dy++)
                {
                    int nx = piece.x + dx;
                    int ny = piece.y + dy;
                    
                    // 检查位置是否有效且为空
                    if (nx >= 0 && nx < ChessBoard.BOARD_SIZE && ny >= 0 && ny < ChessBoard.BOARD_SIZE &&
                        board.GetChessPiece(nx, ny) == 0)
                    {
                        Vector2Int move = new Vector2Int(nx, ny);
                        if (!consideredMoves.Contains(move))
                        {
                            moves.Add(move);
                            consideredMoves.Add(move);
                        }
                    }
                }
            }
        }
        
        return moves;
    }
    
    // 打乱列表
    private void Shuffle<T>(List<T> list)
    {
        int n = list.Count;
        while (n > 1)
        {
            n--;
            int k = random.Next(n + 1);
            T value = list[k];
            list[k] = list[n];
            list[n] = value;
        }
    }
}

这个GomokuAI类实现了一个基于极小极大算法和Alpha-Beta剪枝的五子棋AI。它会评估所有可能的移动,并选择对AI最有利的一步。为了提高效率,它只考虑已有棋子周围的空位,并使用Alpha-Beta剪枝减少搜索空间。此外,它还添加了一些随机性,使AI的行为更加多样化。

16.5.3 AI难度调整与优化

为了适应不同水平的玩家,我们可以添加AI难度调整功能:

csharp

public class GomokuAI
{
    // 难度级别
    public enum DifficultyLevel
    {
        Easy,
        Medium,
        Hard
    }
    
    private ChessBoard board;
    private DifficultyLevel difficulty;
    private Random random;
    
    public GomokuAI(ChessBoard board, DifficultyLevel difficulty = DifficultyLevel.Medium)
    {
        this.board = board;
        this.difficulty = difficulty;
        this.random = new Random();
    }
    
    // 获取AI的下一步移动
    public Vector2Int GetNextMove(int aiPlayer)
    {
        // 如果是第一步,下在天元
        if (IsBoardEmpty())
        {
            return new Vector2Int(7, 7);
        }
        
        // 根据难度确定搜索深度和是否做随机移动
        int maxDepth;
        float randomChance;
        
        switch (difficulty)
        {
            case DifficultyLevel.Easy:
                maxDepth = 1;
                randomChance = 0.3f;
                break;
            case DifficultyLevel.Medium:
                maxDepth = 3;
                randomChance = 0.1f;
                break;
            case DifficultyLevel.Hard:
                maxDepth = 5;
                randomChance = 0.0f;
                break;
            default:
                maxDepth = 3;
                randomChance = 0.1f;
                break;
        }
        
        // 有一定概率做随机移动
        if (random.NextDouble() < randomChance)
        {
            List<Vector2Int> possibleMoves = GenerateMoves();
            if (possibleMoves.Count > 0)
            {
                return possibleMoves[random.Next(possibleMoves.Count)];
            }
        }
        
        // 使用极小极大算法选择最佳移动
        Vector2Int bestMove = Vector2Int.zero;
        int bestScore = int.MinValue;
        
        // 生成可能的移动
        List<Vector2Int> moves = GenerateMoves();
        
        // 打乱顺序,增加随机性
        Shuffle(moves);
        
        // 在简单难度下,只考虑部分移动
        if (difficulty == DifficultyLevel.Easy && moves.Count > 5)
        {
            moves = moves.GetRange(0, 5);
        }
        
        foreach (Vector2Int move in moves)
        {
            // 尝试移动
            board.PlaceChessPiece(move.x, move.y, aiPlayer);
            
            // 评估移动
            int score = MinMax(maxDepth - 1, false, aiPlayer, int.MinValue, int.MaxValue);
            
            // 撤销移动
            board.PlaceChessPiece(move.x, move.y, 0);
            
            if (score > bestScore)
            {
                bestScore = score;
                bestMove = move;
            }
        }
        
        return bestMove;
    }
    
    // 设置难度
    public void SetDifficulty(DifficultyLevel newDifficulty)
    {
        this.difficulty = newDifficulty;
    }
    
    // 其他代码保持不变...
}

这个更新后的GomokuAI类增加了难度级别的概念。不同难度级别有不同的搜索深度和随机性,使得AI在不同难度下表现出不同的棋力。

16.5.4 AI响应速度优化

为了提高AI的响应速度,我们可以使用多线程处理和启发式搜索优化:

csharp

public class GomokuAI : MonoBehaviour
{
    // 其他代码保持不变...
    
    // 异步获取AI的下一步移动
    public async Task<Vector2Int> GetNextMoveAsync(int aiPlayer)
    {
        return await Task.Run(() => GetNextMove(aiPlayer));
    }
    
    // 生成可能的移动(启发式排序)
    private List<Vector2Int> GenerateMovesHeuristic()
    {
        List<MoveScore> moveScores = new List<MoveScore>();
        
        // 获取当前所有棋子的位置
        List<Vector2Int> pieces = new List<Vector2Int>();
        
        for (int x = 0; x < ChessBoard.BOARD_SIZE; x++)
        {
            for (int y = 0; y < ChessBoard.BOARD_SIZE; y++)
            {
                if (board.GetChessPiece(x, y) != 0)
                {
                    pieces.Add(new Vector2Int(x, y));
                }
            }
        }
        
        // 如果棋盘为空,返回中心位置
        if (pieces.Count == 0)
        {
            return new List<Vector2Int> { new Vector2Int(7, 7) };
        }
        
        // 只考虑已有棋子周围的空位
        HashSet<Vector2Int> consideredMoves = new HashSet<Vector2Int>();
        
        foreach (Vector2Int piece in pieces)
        {
            // 检查周围3x3范围内的空位
            for (int dx = -2; dx <= 2; dx++)
            {
                for (int dy = -2; dy <= 2; dy++)
                {
                    int nx = piece.x + dx;
                    int ny = piece.y + dy;
                    
                    // 检查位置是否有效且为空
                    if (nx >= 0 && nx < ChessBoard.BOARD_SIZE && ny >= 0 && ny < ChessBoard.BOARD_SIZE &&
                        board.GetChessPiece(nx, ny) == 0)
                    {
                        Vector2Int move = new Vector2Int(nx, ny);
                        if (!consideredMoves.Contains(move))
                        {
                            // 计算该位置的启发式分数
                            int score = CalculateHeuristicScore(nx, ny);
                            moveScores.Add(new MoveScore { Move = move, Score = score });
                            consideredMoves.Add(move);
                        }
                    }
                }
            }
        }
        
        // 按分数排序(降序)
        moveScores.Sort((a, b) => b.Score.CompareTo(a.Score));
        
        // 转换为移动列表
        List<Vector2Int> sortedMoves = new List<Vector2Int>();
        foreach (MoveScore moveScore in moveScores)
        {
            sortedMoves.Add(moveScore.Move);
        }
        
        return sortedMoves;
    }
    
    // 计算启发式分数
    private int CalculateHeuristicScore(int x, int y)
    {
        // 这里可以实现更复杂的启发式评分
        // 例如,检查是否能形成特定棋型,距离棋盘中心的距离等
        
        // 简单的启发式:与已有棋子的距离越近越好
        int score = 0;
        
        for (int dx = -2; dx <= 2; dx++)
        {
            for (int dy = -2; dy <= 2; dy++)
            {
                int nx = x + dx;
                int ny = y + dy;
                
                if (nx >= 0 && nx < ChessBoard.BOARD_SIZE && ny >= 0 && ny < ChessBoard.BOARD_SIZE)
                {
                    int piece = board.GetChessPiece(nx, ny);
                    if (piece != 0)
                    {
                        // 距离越近,分数越高
                        int distance = Math.Abs(dx) + Math.Abs(dy);
                        score += (3 - distance);
                    }
                }
            }
        }
        
        // 中心位置加分
        int centerDistance = Math.Abs(x - 7) + Math.Abs(y - 7);
        score += (14 - centerDistance) / 2;
        
        return score;
    }
    
    // 移动评分结构
    private struct MoveScore
    {
        public Vector2Int Move;
        public int Score;
    }
}

这个优化版的GomokuAI类增加了异步获取移动的功能,使AI思考时不会阻塞游戏的主线程。它还使用启发式排序来优先考虑更有可能的好移动,从而提高剪枝效率。

16.5.5 集成AI到游戏系统

最后,我们需要将AI集成到游戏系统中:

csharp

public class GameController : MonoBehaviour
{
    public ChessBoardRenderer boardRenderer;
    private GameManager gameManager;
    private ForbiddenMoveChecker forbiddenMoveChecker;
    private GomokuAI ai;
    
    // UI元素
    public Text statusText;
    public GameObject gameOverPanel;
    public Text winnerText;
    
    // 游戏设置面板
    public GameObject settingsPanel;
    public Dropdown difficultyDropdown;
    public Toggle aiFirstToggle;
    
    // 初始化
    void Start()
    {
        // 创建游戏管理器
        gameManager = new GameManager();
        gameManager.SetBoard(boardRenderer.GetBoard());
        
        // 创建禁手检查器
        forbiddenMoveChecker = new ForbiddenMoveChecker(boardRenderer.GetBoard());
        
        // 创建AI
        ai = gameObject.AddComponent<GomokuAI>();
        ai.Initialize(boardRenderer.GetBoard());
        
        // 设置默认难度
        ai.SetDifficulty(GomokuAI.DifficultyLevel.Medium);
        
        // 设置游戏模式为人机对战
        gameManager.SetGameMode(1, 1); // 人执黑
        
        // 初始化设置面板
        InitializeSettingsPanel();
        
        // 隐藏游戏结束面板
        gameOverPanel.SetActive(false);
        
        // 更新状态文本
        UpdateStatusText();
        
        // 如果AI先手,执行AI移动
        if (aiFirstToggle.isOn)
        {
            gameManager.SetGameMode(1, 2); // 人执白
            StartCoroutine(PerformAIMove());
        }
    }
    
    // 初始化设置面板
    private void InitializeSettingsPanel()
    {
        // 设置难度下拉菜单
        difficultyDropdown.onValueChanged.AddListener(OnDifficultyChanged);
        
        // 设置AI先手切换
        aiFirstToggle.onValueChanged.AddListener(OnAIFirstChanged);
        
        // 隐藏设置面板
        settingsPanel.SetActive(false);
    }
    
    // 难度改变回调
    private void OnDifficultyChanged(int value)
    {
        switch (value)
        {
            case 0:
                ai.SetDifficulty(GomokuAI.DifficultyLevel.Easy);
                break;
            case 1:
                ai.SetDifficulty(GomokuAI.DifficultyLevel.Medium);
                break;
            case 2:
                ai.SetDifficulty(GomokuAI.DifficultyLevel.Hard);
                break;
        }
    }
    
    // AI先手改变回调
    private void OnAIFirstChanged(bool isOn)
    {
        // 只在游戏重新开始时生效
    }
    
    // 执行AI移动
    private IEnumerator PerformAIMove()
    {
        // 显示AI思考中
        statusText.text = "AI思考中...";
        
        // 添加思考动画
        GameObject thinkingIcon = CreateThinkingIcon();
        
        // 延迟一段时间,使AI移动看起来更自然
        float thinkTime = Random.Range(0.5f, 1.5f);
        yield return new WaitForSeconds(thinkTime);
        
        // 异步获取AI的下一步移动
        Vector2Int aiMove = await ai.GetNextMoveAsync(gameManager.GetCurrentPlayer());
        
        // 移除思考动画
        Destroy(thinkingIcon);
        
        // 放置棋子
        if (boardRenderer.PlacePiece(aiMove.x, aiMove.y, gameManager.GetCurrentPlayer()))
        {
            // 播放落子音效
            AudioManager.Instance.PlaySound(AudioManager.SoundType.PlacePiece);
            
            // 高亮显示最后一步
            boardRenderer.HighlightLastMove(aiMove.x, aiMove.y);
            
            // 处理移动
            gameManager.HandlePlayerMove(aiMove.x, aiMove.y);
            
            // 检查游戏是否结束
            CheckGameOver();
            
            if (!gameManager.IsGameOver())
            {
                // 切换玩家
                gameManager.SwitchPlayer();
                
                // 更新状态文本
                UpdateStatusText();
            }
        }
    }
    
    // 创建思考图标
    private GameObject CreateThinkingIcon()
    {
        GameObject thinkingIcon = new GameObject("ThinkingIcon");
        thinkingIcon.transform.SetParent(transform);
        
        // 添加图标精灵
        SpriteRenderer renderer = thinkingIcon.AddComponent<SpriteRenderer>();
        renderer.sprite = thinkingSprite;
        
        // 设置位置
        thinkingIcon.transform.position = new Vector3(0, 8, -1);
        
        // 添加旋转动画
        StartCoroutine(AnimateThinking(thinkingIcon));
        
        return thinkingIcon;
    }
    
    // 思考动画
    private IEnumerator AnimateThinking(GameObject thinkingIcon)
    {
        while (thinkingIcon != null)
        {
            thinkingIcon.transform.Rotate(0, 0, -10);
            yield return new WaitForSeconds(0.05f);
        }
    }
    
    // 显示设置面板
    public void ShowSettingsPanel()
    {
        settingsPanel.SetActive(true);
    }
    
    // 隐藏设置面板
    public void HideSettingsPanel()
    {
        settingsPanel.SetActive(false);
    }
    
    // 重新开始游戏
    public void RestartGame()
    {
        // 隐藏游戏结束面板
        gameOverPanel.SetActive(false);
        
        // 清除棋盘
        boardRenderer.ClearBoard();
        
        // 重置游戏状态
        gameManager.ResetGame();
        
        // 设置游戏模式(根据AI先手设置)
        if (aiFirstToggle.isOn)
        {
            gameManager.SetGameMode(1, 2); // 人执白
            StartCoroutine(PerformAIMove());
        }
        else
        {
            gameManager.SetGameMode(1, 1); // 人执黑
        }
        
        // 更新状态文本
        UpdateStatusText();
        
        // 播放游戏开始音效
        AudioManager.Instance.PlaySound(AudioManager.SoundType.GameStart);
    }
    
    // 其他代码保持不变...
}

这段代码将AI集成到游戏系统中,并添加了设置面板,允许玩家选择AI难度和先手顺序。它还添加了AI思考时的视觉反馈,使游戏体验更加完整。

16.6 总结与展望

在本章中,我们详细介绍了如何使用Unity 2021.3.8f1c1引擎开发一个功能完整的五子棋游戏。我们从数学模型和游戏规则出发,实现了棋盘表示、落子交互、胜负判定、禁手规则和AI对战系统等核心功能。

通过本项目,我们学习了以下关键技能和概念:

  1. 棋类游戏的数据结构和算法设计
  2. 二维网格的表示和交互实现
  3. 棋型识别和评估函数设计
  4. 极小极大算法和Alpha-Beta剪枝
  5. 游戏状态管理和流程控制
  6. 用户界面和视觉反馈的实现
  7. AI系统的设计和优化

我们的五子棋游戏已经具备了基本的功能,但还可以进一步扩展和改进,例如:

  1. 添加网络对战功能,使玩家可以与远程对手对弈
  2. 实现游戏录像和回放功能,用于学习和分享
  3. 优化AI算法,使用更先进的技术如蒙特卡洛树搜索
  4. 增加更多的游戏模式,如自定义规则、计时对战等
  5. 改进用户界面和视觉效果,提升游戏体验

五子棋虽然规则简单,但蕴含着丰富的策略和思考。通过开发这个游戏,我们不仅学习了游戏编程的技术,还了解了背后的数学原理和算法思想。这些知识和技能可以应用到其他类型的游戏开发中,为我们的游戏开发之旅打下坚实的基础。

希望本章的内容对你理解棋类游戏的开发有所帮助,激发你对游戏开发和人工智能的兴趣。祝你在游戏开发的道路上取得更大的成功!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐