/// <summary> /// Gets the brush based on the piece given /// </summary> /// <param name="piece">The piece to get a brush for</param> /// <param name="defaultBrush">The color to return if none of the pieces match the case</param> /// <returns>The brush with the same color as the piece</returns> protected virtual Brush GetBrushFromPiece(Pieces piece, Brush defaultBrush) { return(piece switch { Pieces.red => Brushes.Red, Pieces.yellow => Brushes.Yellow, Pieces.draw => Brushes.White, Pieces.green => Brushes.Green, _ => defaultBrush, });
/// <summary> /// Gets the best move for the AI /// </summary> /// <param name="field">The field to look in</param> /// <param name="fieldSize">The size of the field</param> /// <param name="winLength">The length needed to win</param> /// <param name="currentPiece">The piece we are currently looking for</param> /// <param name="opponentPiece">The piece of the opponent, when looking from the current piece</param> /// <param name="depth">How deep we want to look</param> /// <param name="isMax">Whether the player we are looking for is the AI or not (true for AI, false for not AI)</param> /// <returns>The x-position of the best move</returns> protected static int GetBestMove(Pieces[,] field, Size fieldSize, int winLength, Pieces currentPiece, Pieces opponentPiece, int depth, bool isMax) { // If we are at max depth, we are going to score the field if (depth <= 0) { // If the current move is a winning move, give it the best value (relative to the current player) // We also add 100 to the min value, so there is a (small) difference between a losing move for the AI, and "you can't put a piece here" when probing for the y-position if (WinChecker.CheckForWin(field, fieldSize, winLength)) { return(isMax ? (int.MaxValue - searchDepth) : (int.MinValue + searchDepth + 100)); } else { int value = GetFieldValue(field, fieldSize, winLength, currentPiece, opponentPiece); return(value); } } int[] moves = new int[fieldSize.Width]; Pieces[,] testField; // Look through each x-position for (int x = 0; x < fieldSize.Width; x++) { // Find the y-position to put the piece at int y = ProbeY(field, fieldSize, x); // If that position is out of bounds, set it as least desirable for the current player if (y <= 1 || y >= fieldSize.Height) { moves[x] = isMax ? int.MinValue : int.MaxValue; continue; } // Copy the field testField = CopyField(field, fieldSize); // Place a piece at the location found testField[x, y] = currentPiece; // Check if that move was a win, if it is, we can set it as desirable // In order to give a difference in how many moves it takes to get to that win, we lower (or increase for the Min-player) the value by how many layers we are deep if (WinChecker.CheckForWin(testField, fieldSize, winLength)) { moves[x] = isMax ? (int.MaxValue - searchDepth + depth) : (int.MinValue + searchDepth - depth + 100); continue; } // If it was not a winning move, look deeper moves[x] = GetBestMove(testField, fieldSize, winLength, opponentPiece, currentPiece, depth - 1, !isMax); } // Then, after looking at all the moves, the max player returns the maximum value, and the min player the minimum value return(isMax ? moves.Max() : moves.Min()); }
/// <summary> /// Copies the field to a new array /// </summary> /// <param name="field">The field to copy</param> /// <param name="fieldSize">The size of the field</param> /// <returns>A copy of the field</returns> protected static Pieces[,] CopyField(Pieces[,] field, Size fieldSize) { Pieces[,] newField = new Pieces[fieldSize.Width, fieldSize.Height]; for (int x = 0; x < fieldSize.Width; x++) { for (int y = 0; y < fieldSize.Height; y++) { newField[x, y] = field[x, y]; } } return(newField); }
/// <summary> /// Gets the brush based on the piece given /// </summary> /// <param name="piece">The piece to get a brush for</param> /// <returns>The brush with the same color as the piece</returns> protected virtual Brush GetBrushFromPiece(Pieces piece) => GetBrushFromPiece(piece, Settings.backgroundBrush);
/// <summary> /// Resets the playing field /// </summary> /// <param name="createBoard">An override whether to recreate the board [DEFAULT = FALSE]</param> public virtual void Reset(bool createBoard = false) { // If the board size changed, or we want to, we create a new board if (Settings.currentFieldSize != fieldSize || createBoard) { // First we calculate the new sizes and adjust stuff based on those calculations fieldSize = Settings.currentFieldSize; squareSize = CalculateSquareSize(); squareOffset = (int)(squareSize * 0.1f); FallingPiece.fallSpeed = squareSize * (fieldSize.Height / 1.5f); // Then we are ready to construct the board image ConstructBoardImage(); } // If the win-length needed to win changed, we need to check whether that win-length is possible in both axes if (Settings.currentWinLength != winLength) { winLength = Settings.currentWinLength; // Make sure the win-length is reachable both horizontally and vertically if (fieldSize.Height < winLength || fieldSize.Width < winLength) { winLength = Math.Min(fieldSize.Height, fieldSize.Width); ConnectFour.instance.ShowMessage($"You cannot have a win length of {Settings.currentWinLength} on this board! Set to {winLength}", Color.White); } } // Create the new field and reset all important values field = new Pieces[fieldSize.Width, fieldSize.Height]; fallingPiece = null; lockBoard = false; clearBoard = false; wonXpos = new int[winLength]; wonYpos = new int[winLength]; for (int i = 0; i < winLength; i++) { wonXpos[i] = wonYpos[i] = -1; } numPiecesOnBoard = 0; isGameOver = false; random = new Random(); currentPlayer = random.Next(0, 2) switch { 0 => Pieces.red, 1 => Pieces.yellow, _ => Pieces.red, }; }
/// <summary> /// Checks if there is a diagonal win that goes from top-right to bottom-left /// </summary> /// <param name="field">The field to perform the check on</param> /// <param name="fieldSize">The size of the field</param> /// <param name="checkLength">The number of connected pieces to look for</param> /// <returns></returns> protected static int DiagonalLeftValue(Pieces[,] field, Size fieldSize, int checkLength, Pieces piece) { int diagonalValue = 0; int x = fieldSize.Width - 1, y = 0; for (; y <= fieldSize.Height - checkLength; y++) { for (int offset = 0; y + offset <= fieldSize.Height - checkLength; offset++) { Pieces currentColor = field[x - offset, y + offset]; if (currentColor != piece) { continue; } bool uninterrupted = true; for (int length = 1; length < checkLength; length++) { if (field[x - offset - length, y + offset + length] != currentColor) { uninterrupted = false; } } if (uninterrupted) { diagonalValue += checkLength * checkLength; } } } y = 0; // We go 2 back, 1 because the last column has an index of width - 1, and another 1 becuase we already checked the diagonal that starts in the last column x = fieldSize.Width - 2; for (; x > checkLength - 2; x--) { for (int offset = 0; x - offset > checkLength - 2; offset++) { Pieces currentColor = field[x - offset, y + offset]; if (currentColor != piece) { continue; } bool uninterrupted = true; for (int length = 1; length < checkLength; length++) { if (field[x - offset - length, y + offset + length] != currentColor) { uninterrupted = false; } } if (uninterrupted) { diagonalValue += checkLength * checkLength; } } } return(diagonalValue); }
/// <summary> /// Checks if there is a diagonal win that goes from top-left to bottom-right /// </summary> /// <param name="field">The field to perform the check on</param> /// <param name="fieldSize">The size of the field</param> /// <param name="checkLength">The number of connected pieces to look for</param> /// <returns>True if ANY player has 4 diagonally-connected pieces</returns> protected static int DiagonalRightValue(Pieces[,] field, Size fieldSize, int checkLength, Pieces piece) { int diagonalValue = 0; int x = 0, y = 0; // Loop through the three possible right-wards diagonals from the left most line for (; y <= fieldSize.Height - checkLength; y++) { // The 4-long line we look for has up to 3 starting positions, depending on the currentY we start at // To get that value, we take 3 (the max number of positions) and substract the current Y value for (int offset = 0; y + offset <= fieldSize.Height - checkLength; offset++) { // The color of the top-left most piece we are looking at Pieces currentColor = field[x + offset, y + offset]; // If the current color is clear (no piece present), we don't have to look if (currentColor != piece) { continue; } // Look if the four tiles have the same color bool uninterrupted = true; for (int length = 1; length < checkLength; length++) { if (field[x + offset + length, y + offset + length] != currentColor) { uninterrupted = false; } } if (uninterrupted) { diagonalValue += checkLength * checkLength; } } } y = 0; x = 1; for (; x <= fieldSize.Width - checkLength; x++) { for (int offset = 0; x + offset <= fieldSize.Width - checkLength; offset++) { Pieces currentColor = field[x + offset, y + offset]; if (currentColor != piece) { continue; } bool uninterrupted = true; for (int length = 1; length < checkLength; length++) { if (field[x + offset + length, y + offset + length] != currentColor) { uninterrupted = false; } } if (uninterrupted) { diagonalValue += checkLength * checkLength; } } } return(diagonalValue); }
/// <summary> /// Checks if there is a vertical win /// </summary> /// <param name="field">The field to perform the check on</param> /// <param name="fieldSize">The size of the field</param> /// <param name="checkLength">The number of connected pieces to look for</param> /// <returns>True if ANY player has 4 vertically-connected pieces</returns> protected static int VerticalValue(Pieces[,] field, Size fieldSize, int checkLength, Pieces piece) { int verticalValue = 0; for (int x = 0; x < fieldSize.Width; x++) { for (int y = 0; y <= fieldSize.Height - checkLength; y++) { Pieces color = field[x, y]; if (color != piece) { continue; } bool uninterrupted = true; for (int length = 1; length < checkLength; length++) { if (field[x, y + length] != color) { uninterrupted = false; } } if (uninterrupted) { verticalValue += checkLength * checkLength; } } } return(verticalValue); }
/// <summary> /// Gets the horizontal value of the field for a piece /// </summary> /// <param name="field">The field to look in</param> /// <param name="fieldSize">The size of the field</param> /// <param name="checkLength">The length to check for</param> /// <param name="piece">The piece to look for</param> /// <returns></returns> protected static int HorizontalValue(Pieces[,] field, Size fieldSize, int checkLength, Pieces piece) { int horizontalValue = 0; for (int y = fieldSize.Height - 1; y >= 0; y--) { for (int x = 0; x <= fieldSize.Width - checkLength; x++) { Pieces color = field[x, y]; if (color != piece) { continue; } bool uninterrupted = true; for (int length = 1; length < checkLength; length++) { if (field[x + length, y] != color) { uninterrupted = false; } } if (uninterrupted) { horizontalValue += checkLength * checkLength; } } } return(horizontalValue); }
/// <summary> /// Gets the value of a specific field /// </summary> /// <param name="field">The field asses</param> /// <param name="fieldSize">The size of the field</param> /// <param name="winLength">The length needed to win</param> /// <param name="currentPiece">The player to measure the field value for</param> /// <param name="opponentPiece">The opponent of the player</param> /// <returns>The value of the field</returns> protected static int GetFieldValue(Pieces[,] field, Size fieldSize, int winLength, Pieces currentPiece, Pieces opponentPiece) { int fieldValue = 0; // We look at the field and try to find all the 2-pieces, up to 1 less than the win length, and use the same algorithm that looks for a win for (int length = 2; length < winLength; length++) { fieldValue += HorizontalValue(field, fieldSize, length, currentPiece); fieldValue += VerticalValue(field, fieldSize, length, currentPiece); fieldValue += DiagonalLeftValue(field, fieldSize, length, currentPiece); fieldValue += DiagonalRightValue(field, fieldSize, length, currentPiece); fieldValue -= HorizontalValue(field, fieldSize, length, opponentPiece); fieldValue -= VerticalValue(field, fieldSize, length, opponentPiece); fieldValue -= DiagonalLeftValue(field, fieldSize, length, opponentPiece); fieldValue -= DiagonalRightValue(field, fieldSize, length, opponentPiece); } return(fieldValue); }
/// <summary> /// Gets the best move for the AI /// </summary> /// <param name="field">The field to place a piece on</param> /// <param name="fieldSize">The size of the field</param> /// <param name="winLength">The length needed to win a game</param> /// <param name="currentPiece">The current piece to look for (the AI piece)</param> /// <param name="opponentPiece">The piece the opponent uses</param> /// <returns>The x-position of the best move</returns> protected static int GetBestMove(Pieces[,] field, Size fieldSize, int winLength, Pieces currentPiece, Pieces opponentPiece) { int[] moves = new int[fieldSize.Width]; Pieces[,] testField; // Go through each x-position for (int x = 0; x < fieldSize.Width; x++) { // Find the y-position the piece will fall to int y = ProbeY(field, fieldSize, x); // If the piece is out-of-bounds, we can't place a piece here if (y <= -1 || y >= fieldSize.Height) { moves[x] = int.MinValue; continue; } // Makes a copy of the field testField = CopyField(field, fieldSize); // Places the current piece at the place testField[x, y] = currentPiece; // If that placement is a winning move, that means it is a move we really want to make, and that we won't have to look further after this move if (WinChecker.CheckForWin(testField, fieldSize, winLength)) { moves[x] = int.MaxValue; continue; } // If it is not a winning move, look at all the possible moves after this and see how well they hold up moves[x] = GetBestMove(testField, fieldSize, winLength, opponentPiece, currentPiece, searchDepth - 1, false); } // Here, we look for the best index/indeces List <int> bestIndeces = new List <int>(); int bestValue = int.MinValue; for (int i = 0; i < fieldSize.Width; i++) { if (moves[i] > bestValue) { bestIndeces.Clear(); bestIndeces.Add(i); bestValue = moves[i]; } else if (moves[i] == bestValue) { bestIndeces.Add(i); } } int bestIndex = 0; // After we have a list of indeces (which will be at least 1), we either get the best move if there is 1 move, or a random one of the best moves if there are more if (bestIndeces.Count == 1) { bestIndex = bestIndeces[0]; } else if (bestIndeces.Count >= 2) { bestIndex = bestIndeces[random.Next(0, bestIndeces.Count)]; } // There is a random chance the AI will make a random move // We do this at the end so that the AI will always seem to think, even when it will make a random move // We make a random move to give the opponent a more fair chance, otherwhise you would have to "trick" the AI in order to win if (random.Next(0, 100) <= randomMoveChance) { List <int> availableColumns = new List <int>(); for (int x = 0; x < fieldSize.Width; x++) { if (ProbeY(field, fieldSize, x) != -1) { availableColumns.Add(x); } } bestIndex = availableColumns[random.Next(0, availableColumns.Count)]; } return(bestIndex); }