175 lines
6.4 KiB
C#
175 lines
6.4 KiB
C#
namespace CheckersSpielBot
|
|
{
|
|
public class AiService
|
|
{
|
|
private const int MaxDepth = 7;
|
|
private const int WinScore = 100000;
|
|
private const int LoseScore = -100000;
|
|
|
|
private readonly int _aiPlayer;
|
|
private readonly int _humanPlayer;
|
|
|
|
public AiService(int aiPlayer)
|
|
{
|
|
_aiPlayer = aiPlayer;
|
|
_humanPlayer = aiPlayer == 1 ? 2 : 1;
|
|
}
|
|
public Move? GetBestMove(int[,] board)
|
|
{
|
|
Move? bestMove = null;
|
|
int bestScore = int.MinValue;
|
|
|
|
var allMoves = GetAllMovesForPlayer(board, _aiPlayer);
|
|
if (allMoves.Count == 0) return null;
|
|
|
|
foreach (var (originRow, originCol, move) in allMoves)
|
|
{
|
|
int[,] newBoard = ApplyMove(board, originRow, originCol, move);
|
|
|
|
int score = Minimax(newBoard, MaxDepth - 1, int.MinValue, int.MaxValue, false, move.IsCapture, move.DestinationRow, move.DestinationCol);
|
|
|
|
if (score > bestScore)
|
|
{
|
|
bestScore = score;
|
|
bestMove = move;
|
|
}
|
|
}
|
|
|
|
return bestMove;
|
|
}
|
|
|
|
// Minimax with Alpha-Beta Pruning
|
|
private int Minimax(int[,] board, int depth, int alpha, int beta, bool isMaximizing, bool lastMoveWasCapture, int lastMoveRow, int lastMoveCol)
|
|
{
|
|
int currentPlayer = isMaximizing ? _aiPlayer : _humanPlayer;
|
|
|
|
// Chain jump
|
|
if (lastMoveWasCapture)
|
|
{
|
|
var chainJumps = new MoveGeneratorService(board).GetLegalMoves(lastMoveRow, lastMoveCol, capturesOnly: true);
|
|
|
|
if (chainJumps.Count > 0)
|
|
{
|
|
// Same player continue jumping / don't switch turn
|
|
if (isMaximizing)
|
|
{
|
|
int best = int.MinValue;
|
|
foreach (var jump in chainJumps)
|
|
{
|
|
int[,] newBoard = ApplyMove(board, lastMoveRow, lastMoveCol, jump);
|
|
int score = Minimax(newBoard, depth, alpha, beta, true, true, jump.DestinationRow, jump.DestinationCol);
|
|
|
|
best = Math.Max(best, score);
|
|
alpha = Math.Max(alpha, best);
|
|
|
|
if (beta <= alpha) break;
|
|
}
|
|
return best;
|
|
}
|
|
else
|
|
{
|
|
int best = int.MaxValue;
|
|
foreach (var jump in chainJumps)
|
|
{
|
|
int[,] newBoard = ApplyMove(board, lastMoveRow, lastMoveCol, jump);
|
|
int score = Minimax(newBoard, depth, alpha, beta, false, true, jump.DestinationRow, jump.DestinationCol);
|
|
|
|
best = Math.Min(best, score);
|
|
beta = Math.Min(beta, best);
|
|
|
|
if (beta <= alpha) break;
|
|
}
|
|
return best;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Terminal / depth check
|
|
var allMoves = GetAllMovesForPlayer(board, currentPlayer);
|
|
|
|
if (allMoves.Count == 0) { return isMaximizing ? LoseScore : WinScore; }
|
|
|
|
if (depth == 0) { return BoardEvaluator.Evaluate(board, _aiPlayer); }
|
|
|
|
if (isMaximizing)
|
|
{
|
|
int best = int.MinValue;
|
|
foreach (var (oRow, oCol, move) in allMoves)
|
|
{
|
|
int[,] newBoard = ApplyMove(board, oRow, oCol, move);
|
|
int score = Minimax(newBoard, depth - 1, alpha, beta, false, move.IsCapture, move.DestinationRow, move.DestinationCol);
|
|
|
|
best = Math.Max(best, score);
|
|
alpha = Math.Max(alpha, best);
|
|
|
|
if (beta <= alpha) { break; }
|
|
}
|
|
return best;
|
|
}
|
|
else
|
|
{
|
|
int best = int.MaxValue;
|
|
foreach (var (oRow, oCol, move) in allMoves)
|
|
{
|
|
int[,] newBoard = ApplyMove(board, oRow, oCol, move);
|
|
int score = Minimax(newBoard, depth - 1, alpha, beta, true, move.IsCapture, move.DestinationRow, move.DestinationCol);
|
|
|
|
best = Math.Min(best, score);
|
|
beta = Math.Min(beta, best);
|
|
|
|
if (beta <= alpha) { break; }
|
|
}
|
|
return best;
|
|
}
|
|
}
|
|
|
|
#region helpers
|
|
private static List<(int, int, Move)> GetAllMovesForPlayer(int[,] board, int player)
|
|
{
|
|
var moveGen = new MoveGeneratorService(board);
|
|
var captures = new List<(int, int, Move)>();
|
|
var normals = new List<(int, int, Move)>();
|
|
|
|
for (int r = 0; r < 8; r++)
|
|
{
|
|
for (int c = 0; c < 8; c++)
|
|
{
|
|
if (!BoardHelper.BelongsToPlayer(board[r, c], player)) continue;
|
|
|
|
var jumps = moveGen.GetLegalMoves(r, c, capturesOnly: true);
|
|
if (jumps.Count > 0)
|
|
{
|
|
foreach (var j in jumps) captures.Add((r, c, j));
|
|
continue;
|
|
}
|
|
|
|
var moves = moveGen.GetLegalMoves(r, c, capturesOnly: false);
|
|
foreach (var m in moves)
|
|
{
|
|
normals.Add((r, c, m));
|
|
}
|
|
}
|
|
}
|
|
|
|
return captures.Count > 0 ? captures : normals;
|
|
}
|
|
private static int[,] ApplyMove(int[,] board, int originRow, int originCol, Move move)
|
|
{
|
|
int[,] newBoard = (int[,])board.Clone();
|
|
int piece = newBoard[originRow, originCol];
|
|
|
|
newBoard[move.DestinationRow, move.DestinationCol] = piece;
|
|
newBoard[originRow, originCol] = 0;
|
|
|
|
if (move.IsCapture) { newBoard[move.CapturedPieceRow, move.CapturedPieceCol] = 0; }
|
|
|
|
// Promotion
|
|
if (newBoard[move.DestinationRow, move.DestinationCol] == 1 && move.DestinationRow == 0) { newBoard[move.DestinationRow, move.DestinationCol] = 3; }
|
|
|
|
if (newBoard[move.DestinationRow, move.DestinationCol] == 2 && move.DestinationRow == 7) { newBoard[move.DestinationRow, move.DestinationCol] = 4; }
|
|
|
|
return newBoard;
|
|
}
|
|
#endregion
|
|
}
|
|
} |