diff --git a/CheckersSpielBot/AiService.cs b/CheckersSpielBot/AiService.cs new file mode 100644 index 0000000..d1d599e --- /dev/null +++ b/CheckersSpielBot/AiService.cs @@ -0,0 +1,175 @@ +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 + } +} \ No newline at end of file diff --git a/CheckersSpielBot/App.xaml b/CheckersSpielBot/App.xaml index e0afa19..aba1c62 100644 --- a/CheckersSpielBot/App.xaml +++ b/CheckersSpielBot/App.xaml @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CheckersSpielBot" - StartupUri="MainWindow.xaml"> + StartupUri="LoginWindow.xaml"> diff --git a/CheckersSpielBot/App.xaml.cs b/CheckersSpielBot/App.xaml.cs index 3386db9..250649e 100644 --- a/CheckersSpielBot/App.xaml.cs +++ b/CheckersSpielBot/App.xaml.cs @@ -1,14 +1,8 @@ -using System.Configuration; -using System.Data; -using System.Windows; +using System.Windows; namespace CheckersSpielBot { - /// - /// Interaction logic for App.xaml - /// public partial class App : Application { } - -} +} \ No newline at end of file diff --git a/CheckersSpielBot/BoardEvaluator.cs b/CheckersSpielBot/BoardEvaluator.cs new file mode 100644 index 0000000..26eb7b3 --- /dev/null +++ b/CheckersSpielBot/BoardEvaluator.cs @@ -0,0 +1,66 @@ +namespace CheckersSpielBot +{ + public static class BoardEvaluator + { + private const int PieceValue = 100; + private const int KingValue = 160; + + //Weights (Musst be Balanced To have a good Ai) + private const int AdvancementWeight = 5; + private const int BackRowWeight = 15; + private const int CenterWeight = 10; + private const int MobilityWeight = 8; + + public static int Evaluate(int[,] board, int aiPlayer) + { + int score = 0; + int humanPlayer = aiPlayer == 1 ? 2 : 1; + + MoveGeneratorService moveGen = new MoveGeneratorService(board); + + int aiMobility = 0; + int humanMobility = 0; + + for (int row = 0; row < 8; row++) + { + for (int col = 0; col < 8; col++) + { + int piece = board[row, col]; + if (piece == 0) continue; + + //Reward For who will play first + bool isAi = BoardHelper.BelongsToPlayer(piece, aiPlayer); + bool isKing = BoardHelper.IsKing(piece); + + int sign = isAi ? 1 : -1; + + // Material + score += sign * (isKing ? KingValue : PieceValue); + + // Advancement — how far the piece has advanced toward promotion + int advancement = isAi ? (aiPlayer == 1 ? 7 - row : row) : (aiPlayer == 1 ? row : 7 - row); // Red moves up, Blue moves down + + score += sign * advancement * AdvancementWeight; + + // Back row protection — pieces on starting back row + bool onBackRow = isAi ? (aiPlayer == 1 && row == 7) || (aiPlayer == 2 && row == 0) : (aiPlayer == 1 && row == 0) || (aiPlayer == 2 && row == 7); + if (onBackRow && !isKing) { score += sign * BackRowWeight; } + + // Center control — [columns:2,3,4,5 && rows:2,3,4,5] + if (col >= 2 && col <= 5 && row >= 2 && row <= 5) { score += sign * CenterWeight; } + + // Mobility + int moves = moveGen.GetLegalMoves(row, col, capturesOnly: false).Count; + + if (isAi) { aiMobility += moves; } + else { humanMobility += moves; } + } + } + + // Mobility difference + score += (aiMobility - humanMobility) * MobilityWeight; + + return score; + } + } +} \ No newline at end of file diff --git a/CheckersSpielBot/BoardHelper.cs b/CheckersSpielBot/BoardHelper.cs new file mode 100644 index 0000000..e87f030 --- /dev/null +++ b/CheckersSpielBot/BoardHelper.cs @@ -0,0 +1,24 @@ +namespace CheckersSpielBot +{ + public static class BoardHelper + { + public static bool IsPlayableSquare(int row, int col) => (row + col) % 2 == 1; + public static bool IsWithinBounds(int row, int col) => (uint)row < 8 && (uint)col < 8; + + public static bool BelongsToPlayer(int piece, int player) => + player == 1 ? piece == 1 || piece == 3 + : piece == 2 || piece == 4; + + public static bool IsOpponentPiece(int own, int target) + { + if (target == 0) return false; + bool ownRed = own == 1 || own == 3; + bool targetRed = target == 1 || target == 3; + return ownRed != targetRed; + } + + public static bool IsKing(int piece) => piece == 3 || piece == 4; + public static bool IsRedPiece(int piece) => piece == 1 || piece == 3; + public static string PlayerLabel(int player) => player == 1 ? "Red" : "Blue"; + } +} \ No newline at end of file diff --git a/CheckersSpielBot/BoardRenderer.cs b/CheckersSpielBot/BoardRenderer.cs new file mode 100644 index 0000000..0b096fe --- /dev/null +++ b/CheckersSpielBot/BoardRenderer.cs @@ -0,0 +1,126 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace CheckersSpielBot +{ + public class BoardRenderer + { + // Frozen brushes — allocated once + private static readonly SolidColorBrush BrushLightSquare = MakeBrush(0xEE, 0xEE, 0xEE); + private static readonly SolidColorBrush BrushDarkSquare = MakeBrush(0x66, 0x66, 0x66); + private static readonly SolidColorBrush BrushSelected = MakeBrush(0xFF, 0xD7, 0x00); + private static readonly SolidColorBrush BrushValidMove = MakeBrush(0x4C, 0xAF, 0x50); + private static readonly SolidColorBrush BrushMandatory = MakeBrush(0xFF, 0x60, 0x00); + private static readonly SolidColorBrush BrushRedPiece = MakeBrush(0xB4, 0x1E, 0x1E); + private static readonly SolidColorBrush BrushBluePiece = MakeBrush(0x1E, 0x1E, 0xB4); + private static readonly SolidColorBrush BrushKingStar = new SolidColorBrush(Colors.Gold); + + private static SolidColorBrush MakeBrush(byte r, byte g, byte b) + { + var br = new SolidColorBrush(Color.FromRgb(r, g, b)); + br.Freeze(); + return br; + } + + private readonly MainWindow _window; + private readonly GameService _game; + + public BoardRenderer(MainWindow window, GameService game) + { + _window = window; + _game = game; + } + + public void RenderBoard(int selectedRow, int selectedCol, + bool hasPieceSelected, bool isChainJumpActive) + { + for (int row = 0; row < 8; row++) + for (int col = 0; col < 8; col++) + RenderCell(row, col, selectedRow, selectedCol, + hasPieceSelected, isChainJumpActive); + + UpdatePieceCounts(); + } + + private void RenderCell(int row, int col, + int selectedRow, int selectedCol, + bool hasPieceSelected, bool isChainJumpActive) + { + Button btn = GetCellButton(row, col); + if (btn == null) return; + + btn.Background = ResolveCellBackground(row, col, selectedRow, selectedCol, + hasPieceSelected, isChainJumpActive); + btn.Content = BuildPieceVisual(_game.Board[row, col]); + } + + private SolidColorBrush ResolveCellBackground(int row, int col, + int selectedRow, int selectedCol, + bool hasPieceSelected, + bool isChainJumpActive) + { + if (hasPieceSelected && selectedRow == row && selectedCol == col) + return BrushSelected; + + if (hasPieceSelected && !isChainJumpActive && IsValidMoveTarget(row, col, selectedRow, selectedCol)) + return BrushValidMove; + + if (!hasPieceSelected && _game.MandatoryCapturePieces.Contains((row, col))) + return BrushMandatory; + + return BoardHelper.IsPlayableSquare(row, col) ? BrushDarkSquare : BrushLightSquare; + } + + private bool IsValidMoveTarget(int row, int col, int selectedRow, int selectedCol) + { + return _game.GetLegalMoves(selectedRow, selectedCol, capturesOnly: false) + .Exists(m => m.DestinationRow == row && m.DestinationCol == col); + } + + private static UIElement? BuildPieceVisual(int piece) + { + if (piece == 0) return null; + + bool isRed = BoardHelper.IsRedPiece(piece); + bool isKing = BoardHelper.IsKing(piece); + + var circle = new Ellipse + { + Width = 42, + Height = 42, + Fill = isRed ? BrushRedPiece : BrushBluePiece, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + + var grid = new Grid(); + grid.Children.Add(circle); + + if (isKing) + { + grid.Children.Add(new TextBlock + { + Text = "★", + FontSize = 16, + Foreground = BrushKingStar, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }); + } + + return grid; + } + + private void UpdatePieceCounts() + { + var (red, blue) = _game.GetPieceCounts(); + _window.RedScoreText.Text = $"Red: {red}"; + _window.BlueScoreText.Text = $"Blue: {blue}"; + } + + private Button GetCellButton(int row, int col) => + (Button)_window.FindName($"Cell_{row}_{col}"); + } +} \ No newline at end of file diff --git a/CheckersSpielBot/CheckersSpielBot.csproj b/CheckersSpielBot/CheckersSpielBot.csproj index defb4d1..c9baa96 100644 --- a/CheckersSpielBot/CheckersSpielBot.csproj +++ b/CheckersSpielBot/CheckersSpielBot.csproj @@ -8,4 +8,14 @@ true + + + + + + + + + + diff --git a/CheckersSpielBot/DatabaseService.cs b/CheckersSpielBot/DatabaseService.cs new file mode 100644 index 0000000..c6d5aab --- /dev/null +++ b/CheckersSpielBot/DatabaseService.cs @@ -0,0 +1,209 @@ +using System.IO; +using Microsoft.Data.Sqlite; + +namespace CheckersSpielBot +{ + public class DatabaseService + { + private readonly string _connectionString; + + public DatabaseService() + { + string dbPath = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "checkers.db"); + + _connectionString = $"Data Source={dbPath}"; + + InitializeSchema(); + } + + private void InitializeSchema() + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + var cmd = conn.CreateCommand(); + cmd.CommandText = @" + CREATE TABLE IF NOT EXISTS players ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS gameHistory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + playerId INTEGER NOT NULL, + startedAt TEXT NOT NULL DEFAULT (datetime('now')), + endedAt TEXT NULL, + winner TEXT NULL, + wentFirst TEXT NOT NULL DEFAULT 'player', + FOREIGN KEY (playerId) REFERENCES players(id) + );"; + cmd.ExecuteNonQuery(); + } + + public bool RegisterPlayer(string username, string password, out string error) + { + error = string.Empty; + try + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + var check = conn.CreateCommand(); + check.CommandText = "SELECT COUNT(*) FROM players WHERE username = @u"; + check.Parameters.AddWithValue("@u", username); + long exists = (long)check.ExecuteScalar()!; + + if (exists > 0) + { + error = "Username already exists."; + return false; + } + + string hashed = BCrypt.Net.BCrypt.HashPassword(password); + + var cmd = conn.CreateCommand(); + cmd.CommandText = "INSERT INTO players (username, password) VALUES (@u, @p)"; + cmd.Parameters.AddWithValue("@u", username); + cmd.Parameters.AddWithValue("@p", hashed); + cmd.ExecuteNonQuery(); + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public bool LoginPlayer(string username, string password, + out int playerId, out string error) + { + playerId = -1; + error = string.Empty; + try + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT id, password FROM players WHERE username = @u"; + cmd.Parameters.AddWithValue("@u", username); + + using var reader = cmd.ExecuteReader(); + if (!reader.Read()) + { + error = "Username not found."; + return false; + } + + int id = reader.GetInt32(0); + string stored = reader.GetString(1); + + if (!BCrypt.Net.BCrypt.Verify(password, stored)) + { + error = "Incorrect password."; + return false; + } + + playerId = id; + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public int StartGame(int playerId, string wentFirst) + { + try + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + var cmd = conn.CreateCommand(); + cmd.CommandText = @" + INSERT INTO gameHistory (playerId, startedAt, wentFirst) + VALUES (@pid, datetime('now'), @wf); + SELECT last_insert_rowid();"; + cmd.Parameters.AddWithValue("@pid", playerId); + cmd.Parameters.AddWithValue("@wf", wentFirst); + + return Convert.ToInt32(cmd.ExecuteScalar()); + } + catch { return -1; } + } + + public void EndGame(int gameId, string winner) + { + try + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + var cmd = conn.CreateCommand(); + cmd.CommandText = @" + UPDATE gameHistory + SET endedAt = datetime('now'), winner = @w + WHERE id = @id"; + cmd.Parameters.AddWithValue("@w", winner); + cmd.Parameters.AddWithValue("@id", gameId); + cmd.ExecuteNonQuery(); + } + catch { } + } + + public List GetHistory(int playerId) + { + var list = new List(); + try + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + var cmd = conn.CreateCommand(); + cmd.CommandText = @" + SELECT id, startedAt, endedAt, winner, wentFirst + FROM gameHistory + WHERE playerId = @pid + ORDER BY startedAt DESC"; + cmd.Parameters.AddWithValue("@pid", playerId); + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var entry = new GameHistoryEntry + { + Id = reader.GetInt32(0), + StartedAt = DateTime.Parse(reader.GetString(1)), + EndedAt = reader.IsDBNull(2) + ? null + : DateTime.Parse(reader.GetString(2)), + Winner = reader.IsDBNull(3) ? "—" : reader.GetString(3), + WentFirst = reader.GetString(4) + }; + list.Add(entry); + } + } + catch { } + return list; + } + } + + public class GameHistoryEntry + { + public int Id { get; set; } + public DateTime StartedAt { get; set; } + public DateTime? EndedAt { get; set; } + public string Winner { get; set; } = "—"; + public string WentFirst { get; set; } = "—"; + + public string Duration => EndedAt.HasValue + ? $"{(EndedAt.Value - StartedAt).TotalMinutes:F0} min" + : "In progress"; + } +} \ No newline at end of file diff --git a/CheckersSpielBot/Dialogues/HistoryDialog.xaml b/CheckersSpielBot/Dialogues/HistoryDialog.xaml new file mode 100644 index 0000000..0dfb061 --- /dev/null +++ b/CheckersSpielBot/Dialogues/HistoryDialog.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CheckersSpielBot/Dialogues/HistoryDialog.xaml.cs b/CheckersSpielBot/Dialogues/HistoryDialog.xaml.cs new file mode 100644 index 0000000..fcc471c --- /dev/null +++ b/CheckersSpielBot/Dialogues/HistoryDialog.xaml.cs @@ -0,0 +1,16 @@ +using System.Windows; + +namespace CheckersSpielBot +{ + public partial class HistoryDialog : Window + { + public HistoryDialog() + { + InitializeComponent(); + + var db = new DatabaseService(); + var history = db.GetHistory(PlayerSession.PlayerId); + HistoryGrid.ItemsSource = history; + } + } +} \ No newline at end of file diff --git a/CheckersSpielBot/GameModeDialog.xaml b/CheckersSpielBot/GameModeDialog.xaml new file mode 100644 index 0000000..ac2601b --- /dev/null +++ b/CheckersSpielBot/GameModeDialog.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + +