五子棋游戏:基于数学模型的AI对战系统
本文介绍了一个基于Unity引擎开发的五子棋游戏系统,重点阐述了其数学模型和AI对战系统的实现。系统采用15×15棋盘矩阵表示游戏状态,通过二维数组存储棋子分布。核心算法包括:1)胜负判定算法,通过模式匹配检测五子连线;2)禁手规则检查器,实现三三禁手、四四禁手和长连禁手判断;3)AI对战系统,结合极小极大算法与Alpha-Beta剪枝优化,并设计了多难度级别。系统实现了完整的游戏流程控制、交互式
五子棋游戏:基于数学模型的AI对战系统
16.1 五子棋游戏概述与数学基础
五子棋是一种历史悠久的传统棋类游戏,起源于古代中国,后来传播至全世界。它的规则简单明了,却蕴含着丰富的策略和思考,成为众多棋类游戏中较为受欢迎的一种。在五子棋中,两名玩家轮流在棋盘交叉点上放置自己颜色的棋子,先将五颗棋子连成一条直线(横、竖或斜线)的玩家获胜。
从数学角度看,五子棋可以看作是一种在二维平面上的序列匹配问题。棋盘可以表示为一个二维矩阵,其中每个元素代表一个交叉点,元素的值表示该点的状态(空、黑棋或白棋)。游戏的胜利条件可以转化为在矩阵中寻找特定模式的序列。
在本章中,我们将使用Unity 2021.3.8f1c1引擎开发一个功能完整的五子棋游戏,包括棋盘渲染、落子交互、胜负判定以及AI对战系统。我们将结合数学原理和游戏开发技术,从零开始构建这个经典游戏。
16.1.1 五子棋的数学模型
从数学角度来看,五子棋可以建模为以下几个方面:
-
状态空间:15×15的棋盘共有225个交叉点,每个点有三种可能状态(空、黑棋、白棋),理论上总共有3^225种可能的棋局状态。
-
搜索树:每步棋可以视为搜索树中的一个节点,从当前局面到结束局面的所有可能走法构成了一棵巨大的博弈树。
-
评估函数:对于任何给定的棋局,我们可以构建数学函数来评估其对各方的有利程度。这通常基于棋子的分布模式、连续性和潜在威胁等因素。
-
胜利条件:从数学上讲,五子棋的胜利条件可以表述为在二维矩阵中找到长度为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表示白子。通过GetChessPiece和PlaceChessPiece方法,我们可以查询和修改棋盘上特定位置的状态。
16.2.2 五子棋基本规则系统
五子棋的基本规则相对简单,但在程序实现时需要考虑各种情况。以下是五子棋的核心规则:
- 黑方先行,双方轮流在棋盘空位上放置自己的棋子。
- 棋子一旦放置不能移动或移除。
- 任何一方在任意方向(水平、垂直或对角线)连成五个或更多同色棋子时获胜。
- 如果棋盘已满但没有一方达成连五,则游戏为平局。
这些规则可以转化为程序逻辑:
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方法检查给定位置是否是黑方的禁手点,它通过调用CheckOverline、CheckDoubleThree和CheckDoubleFour方法检查长连、三三禁手和四四禁手。这些方法使用模式匹配来检查特定的棋型。
需要注意的是,这是一个简化版的实现。实际的禁手检查会更加复杂,需要考虑更多的棋型和边界情况。
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 前期准备工作
在开始实现游戏之前,我们需要做一些准备工作,包括创建项目、设置场景和导入资源。以下是一个简单的清单:
-
创建一个新的Unity项目(使用Unity 2021.3.8f1c1版本)。
-
创建以下文件夹结构:
- Scripts(存放脚本文件)
- Prefabs(存放预制体)
- Sprites(存放贴图)
- Scenes(存放场景)
- Materials(存放材质)
-
导入或创建所需的素材:
- 棋盘背景贴图
- 黑棋和白棋的贴图
- UI元素(按钮、面板等)
-
创建一个新场景,命名为"GomokuGame"。
接下来,我们将实现游戏的各个组件。
16.4.2 游戏场景搭建
首先,我们需要搭建游戏的基本场景结构:
-
设置摄像机:
csharp
// 设置主相机 Camera.main.transform.position = new Vector3(0, 0, -10); Camera.main.orthographic = true; Camera.main.orthographicSize = 10; -
创建棋盘对象:
csharp
// 创建棋盘对象 GameObject boardObject = new GameObject("ChessBoard"); boardObject.AddComponent<ChessBoardRenderer>(); -
创建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); -
添加游戏管理器:
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 落子操作与交互实现
落子操作是游戏的核心交互。我们需要实现以下功能:
- 检测鼠标点击
- 将鼠标坐标转换为棋盘坐标
- 验证移动是否合法
- 放置棋子
- 处理游戏逻辑
以下是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类增加了游戏模式和玩家颜色的概念,支持人人对战和人机对战两种模式。IsAITurn和IsPlayerTurn方法根据当前游戏模式和玩家颜色判断当前是否是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对战系统等核心功能。
通过本项目,我们学习了以下关键技能和概念:
- 棋类游戏的数据结构和算法设计
- 二维网格的表示和交互实现
- 棋型识别和评估函数设计
- 极小极大算法和Alpha-Beta剪枝
- 游戏状态管理和流程控制
- 用户界面和视觉反馈的实现
- AI系统的设计和优化
我们的五子棋游戏已经具备了基本的功能,但还可以进一步扩展和改进,例如:
- 添加网络对战功能,使玩家可以与远程对手对弈
- 实现游戏录像和回放功能,用于学习和分享
- 优化AI算法,使用更先进的技术如蒙特卡洛树搜索
- 增加更多的游戏模式,如自定义规则、计时对战等
- 改进用户界面和视觉效果,提升游戏体验
五子棋虽然规则简单,但蕴含着丰富的策略和思考。通过开发这个游戏,我们不仅学习了游戏编程的技术,还了解了背后的数学原理和算法思想。这些知识和技能可以应用到其他类型的游戏开发中,为我们的游戏开发之旅打下坚实的基础。
希望本章的内容对你理解棋类游戏的开发有所帮助,激发你对游戏开发和人工智能的兴趣。祝你在游戏开发的道路上取得更大的成功!
更多推荐


所有评论(0)