我正在尝试为四子棋(或称为Connect4)游戏实现MinMax算法。
我认为我已经理解了它的原理,它应该构建一个可能的棋盘树到一定深度,评估它们并返回它们的分数,然后我们只需取这些分数的最大值。
因此,aiChooseCol()
通过调用MinMax()
检查每个可能的列的分数,并返回具有最高分数的列。
现在我不确定,这样调用MinMax()
是否正确?
检查temp = Math.Max(temp, 1000);
是否正确?
我还没有制作启发式函数,但这至少应该能识别出获胜的列并选择它,但目前它只是从左边选择第一个空闲的列…我搞不清楚我做错了什么。
private int AiChooseCol(){ int best = -1000; int col=0; for (int i = 0; i < m_Board.Cols; i++) { if (m_Board.CheckIfColHasRoom(i)) { m_Board.FillSignInBoardAccordingToCol(i, m_Sign); int t = MinMax(5, m_Board, board.GetOtherPlayerSign(m_Sign)); if (t > best) { best = t; col = i; } m_Board.RemoveTopCoinFromCol(i); } } return col;}private int MinMax(int Depth, board Board, char PlayerSign){ int temp=0; if (Depth <= 0) { // 从启发式函数返回 return temp; } char otherPlayerSign = board.GetOtherPlayerSign(PlayerSign); char checkBoard = Board.CheckBoardForWin(); if (checkBoard == PlayerSign) { return 1000; } else if (checkBoard == otherPlayerSign) { return -1000; } else if (!Board.CheckIfBoardIsNotFull()) { return 0; // 平局 } if (PlayerSign == m_Sign) // 最大化玩家是我自己 { temp = -1000; for (int i = 0; i < Board.Cols; i++) { if (Board.FillSignInBoardAccordingToCol(i, PlayerSign)) // 所以我们不会在满列中打开另一个分支 { var v = MinMax(Depth - 1, Board, otherPlayerSign); temp = Math.Max(temp, v); Board.RemoveTopCoinFromCol(i); } } } else { temp = 1000; for (int i = 0; i < Board.Cols; i++) { if (Board.FillSignInBoardAccordingToCol(i, PlayerSign)) // 所以我们不会在满列中打开另一个分支 { var v = MinMax(Depth - 1, Board, otherPlayerSign); temp = Math.Min(temp, v); Board.RemoveTopCoinFromCol(i); } } } return temp;}
一些说明:
FillSignInBoardAccordingToCol()
如果成功会返回一个布尔值。
board
类型有一个char[,]
数组,包含实际的棋盘和玩家的标记。
这段代码位于AI玩家类中。
回答:
因此,我决定编写自己的MinMax Connect 4算法。我使用深度来确定赢或输的价值,以便更接近赢或阻止输的移动会优先考虑。我还决定,如果有多个移动具有相同的启发式值,我将随机选择一个移动。最后,我将深度扩展到6,因为从开始需要六个移动来找到可能的赢路径。
private static void Main(string[] args){ var board = new Board(8,7); var random = new Random(); while (true) { Console.WriteLine("选择列 1 - 8"); int move; if (!int.TryParse(Console.ReadLine(), out move) || move < 1 || move > 8) { Console.WriteLine("必须输入1到8之间的数字。"); continue; } if (!board.DropCoin(1, move-1)) { Console.WriteLine("该列已满,请选择另一列"); continue; } if (board.Winner == 1) { Console.WriteLine(board); Console.WriteLine("你赢了!"); break; } if (board.IsFull) { Console.WriteLine(board); Console.WriteLine("平局!"); break; } var moves = new List<Tuple<int, int>>(); for (int i = 0; i < board.Columns; i++) { if (!board.DropCoin(2, i)) continue; moves.Add(Tuple.Create(i, MinMax(6, board, false))); board.RemoveTopCoin(i); } int maxMoveScore = moves.Max(t => t.Item2); var bestMoves = moves.Where(t => t.Item2 == maxMoveScore).ToList(); board.DropCoin(2, bestMoves[random.Next(0,bestMoves.Count)].Item1); Console.WriteLine(board); if (board.Winner == 2) { Console.WriteLine("你输了!"); break; } if (board.IsFull) { Console.WriteLine("平局!"); break; } } Console.WriteLine("完成"); Console.ReadKey();}private static int MinMax(int depth, Board board, bool maximizingPlayer){ if (depth <= 0) return 0; var winner = board.Winner; if (winner == 2) return depth; if (winner == 1) return -depth; if (board.IsFull) return 0; int bestValue = maximizingPlayer ? -1 : 1; for (int i = 0; i < board.Columns; i++) { if (!board.DropCoin(maximizingPlayer ? 2 : 1, i)) continue; int v = MinMax(depth - 1, board, !maximizingPlayer); bestValue = maximizingPlayer ? Math.Max(bestValue, v) : Math.Min(bestValue, v); board.RemoveTopCoin(i); } return bestValue;}public class Board{ private readonly int?[,] _board; private int? _winner; private bool _changed; public Board(int cols, int rows) { Columns = cols; Rows = rows; _board = new int?[cols, rows]; } public int Columns { get; } public int Rows { get; } public bool ColumnFree(int column) { return !_board[column, 0].HasValue; } public bool DropCoin(int playerId, int column) { int row = 0; while (row < Rows && !_board[column,row].HasValue) { row++; } if (row == 0) return false; _board[column, row - 1] = playerId; _changed = true; return true; } public bool RemoveTopCoin(int column) { int row = 0; while (row < Rows && !_board[column, row].HasValue) { row++; } if (row == Rows) return false; _board[column, row] = null; _changed = true; return true; } public int? Winner { get { if (!_changed) return _winner; _changed = false; for (int i = 0; i < Columns; i++) { for (int j = 0; j < Rows; j++) { if (!_board[i, j].HasValue) continue; bool horizontal = i + 3 < Columns; bool vertical = j + 3 < Rows; if (!horizontal && !vertical) continue; bool forwardDiagonal = horizontal && vertical; bool backwardDiagonal = vertical && i - 3 >= 0; for (int k = 1; k < 4; k++) { horizontal = horizontal && _board[i, j] == _board[i + k, j]; vertical = vertical && _board[i, j] == _board[i, j + k]; forwardDiagonal = forwardDiagonal && _board[i, j] == _board[i + k, j + k]; backwardDiagonal = backwardDiagonal && _board[i, j] == _board[i - k, j + k]; if (!horizontal && !vertical && !forwardDiagonal && !backwardDiagonal) break; } if (horizontal || vertical || forwardDiagonal || backwardDiagonal) { _winner = _board[i, j]; return _winner; } } } _winner = null; return _winner; } } public bool IsFull { get { for (int i = 0; i < Columns; i++) { if (!_board[i, 0].HasValue) return false; } return true; } } public override string ToString() { var builder = new StringBuilder(); for (int j = 0; j < Rows; j++) { builder.Append('|'); for (int i = 0; i < Columns; i++) { builder.Append(_board[i, j].HasValue ? _board[i,j].Value.ToString() : " ").Append('|'); } builder.AppendLine(); } return builder.ToString(); }}