This commit is contained in:
abdelaziz
2026-03-30 07:49:37 +02:00
parent 9440351bc0
commit bc4118704e
21 changed files with 1785 additions and 124 deletions
+175
View File
@@ -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
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CheckersSpielBot" xmlns:local="clr-namespace:CheckersSpielBot"
StartupUri="MainWindow.xaml"> StartupUri="LoginWindow.xaml">
<Application.Resources> <Application.Resources>
</Application.Resources> </Application.Resources>
+1 -7
View File
@@ -1,14 +1,8 @@
using System.Configuration; using System.Windows;
using System.Data;
using System.Windows;
namespace CheckersSpielBot namespace CheckersSpielBot
{ {
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application public partial class App : Application
{ {
} }
} }
+66
View File
@@ -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;
}
}
}
+24
View File
@@ -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";
}
}
+126
View File
@@ -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}");
}
}
+10
View File
@@ -8,4 +8,14 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
<PackageReference Include="MySql.Data" Version="9.6.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\" />
</ItemGroup>
</Project> </Project>
+209
View File
@@ -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<GameHistoryEntry> GetHistory(int playerId)
{
var list = new List<GameHistoryEntry>();
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";
}
}
@@ -0,0 +1,84 @@
<Window x:Class="CheckersSpielBot.HistoryDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Game History" Height="450" Width="700"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
Background="#222">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="14"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="Your Game History"
Foreground="White"
FontSize="20"
FontWeight="Bold"
HorizontalAlignment="Center"/>
<DataGrid Grid.Row="2"
x:Name="HistoryGrid"
AutoGenerateColumns="False"
IsReadOnly="True"
Background="#2A2A2A"
Foreground="White"
BorderBrush="#444"
RowBackground="#2A2A2A"
AlternatingRowBackground="#333"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#444"
HeadersVisibility="Column"
CanUserResizeRows="False"
SelectionMode="Single">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#333"/>
<Setter Property="Foreground" Value="#CCC"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="BorderBrush" Value="#444"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Padding" Value="8,4"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="White"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#444"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="# "
Binding="{Binding Id}"
Width="50"/>
<DataGridTextColumn Header="Started"
Binding="{Binding StartedAt, StringFormat='{}{0:dd.MM.yyyy HH:mm}'}"
Width="140"/>
<DataGridTextColumn Header="Ended"
Binding="{Binding EndedAt, StringFormat='{}{0:dd.MM.yyyy HH:mm}'}"
Width="140"/>
<DataGridTextColumn Header="Duration"
Binding="{Binding Duration}"
Width="90"/>
<DataGridTextColumn Header="Went First"
Binding="{Binding WentFirst}"
Width="100"/>
<DataGridTextColumn Header="Winner"
Binding="{Binding Winner}"
Width="*"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
@@ -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;
}
}
}
+104
View File
@@ -0,0 +1,104 @@
<Window x:Class="CheckersSpielBot.GameModeDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Checkers" Height="300" Width="400"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
Background="#222">
<Window.Resources>
<Style x:Key="RedButton" TargetType="Button">
<Setter Property="Background" Value="#CC2222"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#AA1111"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#880000"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="BlueButton" TargetType="Button">
<Setter Property="Background" Value="#2255CC"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#1133AA"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#002288"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid Margin="30">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="20"/>
<RowDefinition Height="50"/>
<RowDefinition Height="12"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="Who goes first?"
Foreground="White"
FontSize="22"
FontWeight="Bold"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Grid.Row="1"
Text="Your color matches your choice"
Foreground="#888"
FontSize="12"
HorizontalAlignment="Center"/>
<Button Grid.Row="3"
Content="I go first — I play as Red"
Style="{StaticResource RedButton}"
Click="HumanFirst_Click"/>
<Button Grid.Row="5"
Content="Bot goes first — I play as Blue"
Style="{StaticResource BlueButton}"
Click="BotFirst_Click"/>
</Grid>
</Window>
+26
View File
@@ -0,0 +1,26 @@
using System.Windows;
namespace CheckersSpielBot
{
public partial class GameModeDialog : Window
{
public bool HumanGoesFirst { get; private set; } = true;
public GameModeDialog()
{
InitializeComponent();
}
private void HumanFirst_Click(object sender, RoutedEventArgs e)
{
HumanGoesFirst = true;
Close();
}
private void BotFirst_Click(object sender, RoutedEventArgs e)
{
HumanGoesFirst = false;
Close();
}
}
}
+192
View File
@@ -0,0 +1,192 @@
namespace CheckersSpielBot
{
public class GameService
{
// 0=Empty | 1=Red | 2=Blue | 3=RedKing | 4=BlueKing
public int[,] Board { get; private set; } = new int[8, 8];
public int ActivePlayer { get; private set; } = 1;
public bool IsChainJumpActive { get; private set; } = false;
public int ChainJumpRow { get; private set; } = -1;
public int ChainJumpCol { get; private set; } = -1;
public HashSet<(int, int)> MandatoryCapturePieces { get; private set; } = new();
private MoveGeneratorService _moveGenerator;
public GameService()
{
_moveGenerator = new MoveGeneratorService(Board);
}
// Setup
public void InitializeNewGame()
{
Board = new int[8, 8];
ActivePlayer = 1;
IsChainJumpActive = false;
ChainJumpRow = -1;
ChainJumpCol = -1;
_moveGenerator = new MoveGeneratorService(Board);
PlaceInitialPieces();
RefreshMandatoryCaptures();
}
private void PlaceInitialPieces()
{
for (int row = 0; row < 3; row++)
for (int col = 0; col < 8; col++)
if (BoardHelper.IsPlayableSquare(row, col))
Board[row, col] = 2; // Blue top
for (int row = 5; row < 8; row++)
for (int col = 0; col < 8; col++)
if (BoardHelper.IsPlayableSquare(row, col))
Board[row, col] = 1; // Red bottom
}
// Move generation
public List<Move> GetLegalMoves(int row, int col, bool capturesOnly) =>
_moveGenerator.GetLegalMoves(row, col, capturesOnly);
// Move execution
public MoveResult ExecuteMove(int originRow, int originCol, Move move)
{
int piece = Board[originRow, originCol];
Board[move.DestinationRow, move.DestinationCol] = piece;
Board[originRow, originCol] = 0;
if (move.IsCapture)
Board[move.CapturedPieceRow, move.CapturedPieceCol] = 0;
PromoteToKingIfEligible(move.DestinationRow, move.DestinationCol);
ResetChainJump();
// Chain jump
if (move.IsCapture)
{
var furtherJumps = GetLegalMoves(move.DestinationRow, move.DestinationCol,
capturesOnly: true);
if (furtherJumps.Count > 0)
{
IsChainJumpActive = true;
ChainJumpRow = move.DestinationRow;
ChainJumpCol = move.DestinationCol;
return MoveResult.ChainJumpRequired;
}
}
// Victory check
if (EvaluateVictoryCondition(out int victor))
return MoveResult.Victory(victor);
// Advance turn
ActivePlayer = ActivePlayer == 1 ? 2 : 1;
RefreshMandatoryCaptures();
return MoveResult.TurnAdvanced;
}
private void PromoteToKingIfEligible(int row, int col)
{
if (Board[row, col] == 1 && row == 0) Board[row, col] = 3;
if (Board[row, col] == 2 && row == 7) Board[row, col] = 4;
}
private void ResetChainJump()
{
IsChainJumpActive = false;
ChainJumpRow = -1;
ChainJumpCol = -1;
}
// Mandatory captures
public void RefreshMandatoryCaptures()
{
MandatoryCapturePieces = new HashSet<(int, int)>();
for (int r = 0; r < 8; r++)
for (int c = 0; c < 8; c++)
{
int p = Board[r, c];
if (BoardHelper.BelongsToPlayer(p, ActivePlayer) &&
GetLegalMoves(r, c, capturesOnly: true).Count > 0)
MandatoryCapturePieces.Add((r, c));
}
}
// Victory
private bool EvaluateVictoryCondition(out int victor)
{
victor = 0;
int opponent = ActivePlayer == 1 ? 2 : 1;
bool opponentHasPieces = false;
bool opponentCanMove = false;
for (int r = 0; r < 8; r++)
{
for (int c = 0; c < 8; c++)
{
if (!BoardHelper.BelongsToPlayer(Board[r, c], opponent)) continue;
opponentHasPieces = true;
if (GetLegalMoves(r, c, capturesOnly: false).Count > 0)
{
opponentCanMove = true;
break;
}
}
if (opponentCanMove) break;
}
if (!opponentHasPieces || !opponentCanMove)
{
victor = ActivePlayer;
return true;
}
return false;
}
public List<Move> GetAllLegalMovesForPlayer(int player)
{
var moveGen = new MoveGeneratorService(Board);
var captures = new List<Move>();
var normals = new List<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) { captures.AddRange(jumps); continue; }
normals.AddRange(moveGen.GetLegalMoves(r, c, capturesOnly: false));
}
}
return captures.Count > 0 ? captures : normals;
}
#region helpers
public bool BelongsToActivePlayer(int piece) =>
BoardHelper.BelongsToPlayer(piece, ActivePlayer);
public (int red, int blue) GetPieceCounts()
{
int red = 0, blue = 0;
for (int r = 0; r < 8; r++)
for (int c = 0; c < 8; c++)
{
int p = Board[r, c];
if (p == 1 || p == 3) red++;
else if (p == 2 || p == 4) blue++;
}
return (red, blue);
}
#endregion
}
}
+166
View File
@@ -0,0 +1,166 @@
<Window x:Class="CheckersSpielBot.LoginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Checkers" Height="420" Width="360"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
Background="#222">
<Window.Resources>
<Style x:Key="InputStyle" TargetType="TextBox">
<Setter Property="Background" Value="#333"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#555"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Height" Value="36"/>
<Setter Property="CaretBrush" Value="White"/>
</Style>
<Style x:Key="PasswordStyle" TargetType="PasswordBox">
<Setter Property="Background" Value="#333"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#555"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Height" Value="36"/>
</Style>
<Style x:Key="PrimaryBtn" TargetType="Button">
<Setter Property="Background" Value="#CC2222"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Height" Value="42"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="5">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#AA1111"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#880000"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TabBtn" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="#888"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="Transparent"
BorderBrush="#555"
BorderThickness="0,0,0,2"
Padding="0,0,0,4">
<ContentPresenter HorizontalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="White"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid Margin="30">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="20"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="14"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Title -->
<TextBlock Grid.Row="0"
Text="CHECKERS"
Foreground="White"
FontSize="24"
FontWeight="Bold"
HorizontalAlignment="Center"/>
<!-- Tab switcher -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
x:Name="TabLoginBtn"
Content="Login"
Style="{StaticResource TabBtn}"
Click="SwitchToLogin"/>
<Button Grid.Column="1"
x:Name="TabRegisterBtn"
Content="Register"
Style="{StaticResource TabBtn}"
Click="SwitchToRegister"/>
</Grid>
<!-- Username -->
<TextBlock Grid.Row="4"
Text="Username"
Foreground="#AAA"
FontSize="12"/>
<TextBox Grid.Row="6"
x:Name="UsernameBox"
Style="{StaticResource InputStyle}"/>
<!-- Password -->
<TextBlock Grid.Row="8"
Text="Password"
Foreground="#AAA"
FontSize="12"/>
<PasswordBox Grid.Row="10"
x:Name="PwdBox"
Style="{StaticResource PasswordStyle}"/>
<!-- Error / success -->
<TextBlock Grid.Row="12"
x:Name="ErrorText"
Foreground="#FF6666"
FontSize="12"
TextWrapping="Wrap"
HorizontalAlignment="Center"/>
<Button Grid.Row="12"
x:Name="ActionBtn"
Content="Login"
Style="{StaticResource PrimaryBtn}"
Margin="0,24,0,0"
Click="ActionBtn_Click"/>
</Grid>
</Window>
+128
View File
@@ -0,0 +1,128 @@
using System.Windows;
using System.Windows.Media;
namespace CheckersSpielBot
{
public partial class LoginWindow : Window
{
private readonly DatabaseService _db = new();
private bool _isLoginMode = true;
public LoginWindow()
{
InitializeComponent();
ApplyTabStyle();
}
#region Tab switching
private void SwitchToLogin(object sender, RoutedEventArgs e)
{
_isLoginMode = true;
ActionBtn.Content = "Login";
ErrorText.Text = string.Empty;
PwdBox.Clear();
ApplyTabStyle();
}
private void SwitchToRegister(object sender, RoutedEventArgs e)
{
_isLoginMode = false;
ActionBtn.Content = "Register";
ErrorText.Text = string.Empty;
PwdBox.Clear();
ApplyTabStyle();
}
private void ApplyTabStyle()
{
// Active tab — white text, red underline
// Inactive tab — grey text, grey underline
if (_isLoginMode)
{
TabLoginBtn.Foreground = Brushes.White;
TabRegisterBtn.Foreground = new SolidColorBrush(Color.FromRgb(0x88, 0x88, 0x88));
}
else
{
TabLoginBtn.Foreground = new SolidColorBrush(Color.FromRgb(0x88, 0x88, 0x88));
TabRegisterBtn.Foreground = Brushes.White;
}
}
#endregion
#region Actions
private void ActionBtn_Click(object sender, RoutedEventArgs e)
{
if (_isLoginMode) HandleLogin();
else HandleRegister();
}
private void HandleLogin()
{
string username = UsernameBox.Text.Trim();
string password = PwdBox.Password;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
ShowError("Please enter username and password.");
return;
}
if (!_db.LoginPlayer(username, password, out int playerId, out string error))
{
ShowError(error);
return;
}
PlayerSession.PlayerId = playerId;
PlayerSession.Username = username;
var mainWindow = new MainWindow();
mainWindow.Show();
Close();
}
private void HandleRegister()
{
string username = UsernameBox.Text.Trim();
string password = PwdBox.Password;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
ShowError("Please enter username and password.");
return;
}
if (username.Length < 3)
{
ShowError("Username must be at least 3 characters.");
return;
}
if (password.Length < 6)
{
ShowError("Password must be at least 6 characters.");
return;
}
if (!_db.RegisterPlayer(username, password, out string error))
{
ShowError(error);
return;
}
ErrorText.Foreground = Brushes.LightGreen;
ErrorText.Text = "Registered! You can now login.";
SwitchToLogin(null!, null!);
}
#endregion
#region Helpers
private void ShowError(string message)
{
ErrorText.Foreground = Brushes.OrangeRed;
ErrorText.Text = message;
}
#endregion
}
}
+92 -113
View File
@@ -1,20 +1,13 @@
<Window x:Class="CheckersGame.MainWindow" <Window x:Class="CheckersSpielBot.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" mc:Ignorable="d"
Title="Checkers Game" Height="750" Width="650" Title="Checkers Game" Height="750" Width="650"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
Background="#222"
Background="#222"> Closing="Window_Closing">
<Grid Margin="15"> <Grid Margin="15">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -23,162 +16,148 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Top Panel -->
<Border Grid.Row="0" <Border Grid.Row="0"
Background="#333" Background="#333"
CornerRadius="10" CornerRadius="10"
Padding="12" Padding="12"
Margin="0,0,0,15"> Margin="0,0,0,15">
<DockPanel> <DockPanel>
<TextBlock Text="Checkers" <TextBlock Text="Checkers"
Foreground="White" Foreground="White"
FontSize="28" FontSize="28"
FontWeight="Bold" FontWeight="Bold"
DockPanel.Dock="Left" DockPanel.Dock="Left"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
HorizontalAlignment="Right" HorizontalAlignment="Right"
DockPanel.Dock="Right"
VerticalAlignment="Center">
<TextBlock x:Name="RedScoreText"
Text="Red: 12"
Foreground="#FF6666"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="0,0,14,0"/>
<TextBlock x:Name="BlueScoreText"
Text="Blue: 12"
Foreground="#6699FF"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="0,0,14,0"/>
<Button Content="History"
Width="100"
Height="35"
Margin="5"
Click="History_Click"/>
DockPanel.Dock="Right">
<Button Content="New Game" <Button Content="New Game"
Width="100" Width="100"
Height="35" Height="35"
Margin="5" Margin="5"
Click="NewGame"/>
Click="NewGame_Click"/>
<Button Content="Reset"
Width="100"
Height="35"
Margin="5"
Click="Reset_Click"/>
</StackPanel> </StackPanel>
</DockPanel> </DockPanel>
</Border> </Border>
<Border Grid.Row="1" <Border Grid.Row="1"
Background="#111" Background="#111"
CornerRadius="12" CornerRadius="12"
Padding="10"> Padding="10">
<UniformGrid Rows="8" Columns="8"> <UniformGrid Rows="8" Columns="8">
<Button x:Name="Cell_0_0" Click="Cell_Click" Tag="0,0" Background="#EEE"/> <Button x:Name="Cell_0_0" Click="CellClick" Tag="0,0"/>
<Button x:Name="Cell_0_1" Click="Cell_Click" Tag="0,1" Background="#666"/> <Button x:Name="Cell_0_1" Click="CellClick" Tag="0,1"/>
<Button x:Name="Cell_0_2" Click="Cell_Click" Tag="0,2" Background="#EEE"/> <Button x:Name="Cell_0_2" Click="CellClick" Tag="0,2"/>
<Button x:Name="Cell_0_3" Click="Cell_Click" Tag="0,3" Background="#666"/> <Button x:Name="Cell_0_3" Click="CellClick" Tag="0,3"/>
<Button x:Name="Cell_0_4" Click="Cell_Click" Tag="0,4" Background="#EEE"/> <Button x:Name="Cell_0_4" Click="CellClick" Tag="0,4"/>
<Button x:Name="Cell_0_5" Click="Cell_Click" Tag="0,5" Background="#666"/> <Button x:Name="Cell_0_5" Click="CellClick" Tag="0,5"/>
<Button x:Name="Cell_0_6" Click="Cell_Click" Tag="0,6" Background="#EEE"/> <Button x:Name="Cell_0_6" Click="CellClick" Tag="0,6"/>
<Button x:Name="Cell_0_7" Click="Cell_Click" Tag="0,7" Background="#666"/> <Button x:Name="Cell_0_7" Click="CellClick" Tag="0,7"/>
<Button x:Name="Cell_1_0" Click="Cell_Click" Tag="1,0" Background="#666"/> <Button x:Name="Cell_1_0" Click="CellClick" Tag="1,0"/>
<Button x:Name="Cell_1_1" Click="Cell_Click" Tag="1,1" Background="#EEE"/> <Button x:Name="Cell_1_1" Click="CellClick" Tag="1,1"/>
<Button x:Name="Cell_1_2" Click="Cell_Click" Tag="1,2" Background="#666"/> <Button x:Name="Cell_1_2" Click="CellClick" Tag="1,2"/>
<Button x:Name="Cell_1_3" Click="Cell_Click" Tag="1,3" Background="#EEE"/> <Button x:Name="Cell_1_3" Click="CellClick" Tag="1,3"/>
<Button x:Name="Cell_1_4" Click="Cell_Click" Tag="1,4" Background="#666"/> <Button x:Name="Cell_1_4" Click="CellClick" Tag="1,4"/>
<Button x:Name="Cell_1_5" Click="Cell_Click" Tag="1,5" Background="#EEE"/> <Button x:Name="Cell_1_5" Click="CellClick" Tag="1,5"/>
<Button x:Name="Cell_1_6" Click="Cell_Click" Tag="1,6" Background="#666"/> <Button x:Name="Cell_1_6" Click="CellClick" Tag="1,6"/>
<Button x:Name="Cell_1_7" Click="Cell_Click" Tag="1,7" Background="#EEE"/> <Button x:Name="Cell_1_7" Click="CellClick" Tag="1,7"/>
<Button x:Name="Cell_2_0" Click="Cell_Click" Tag="2,0" Background="#EEE"/> <Button x:Name="Cell_2_0" Click="CellClick" Tag="2,0"/>
<Button x:Name="Cell_2_1" Click="Cell_Click" Tag="2,1" Background="#666"/> <Button x:Name="Cell_2_1" Click="CellClick" Tag="2,1"/>
<Button x:Name="Cell_2_2" Click="Cell_Click" Tag="2,2" Background="#EEE"/> <Button x:Name="Cell_2_2" Click="CellClick" Tag="2,2"/>
<Button x:Name="Cell_2_3" Click="Cell_Click" Tag="2,3" Background="#666"/> <Button x:Name="Cell_2_3" Click="CellClick" Tag="2,3"/>
<Button x:Name="Cell_2_4" Click="Cell_Click" Tag="2,4" Background="#EEE"/> <Button x:Name="Cell_2_4" Click="CellClick" Tag="2,4"/>
<Button x:Name="Cell_2_5" Click="Cell_Click" Tag="2,5" Background="#666"/> <Button x:Name="Cell_2_5" Click="CellClick" Tag="2,5"/>
<Button x:Name="Cell_2_6" Click="Cell_Click" Tag="2,6" Background="#EEE"/> <Button x:Name="Cell_2_6" Click="CellClick" Tag="2,6"/>
<Button x:Name="Cell_2_7" Click="Cell_Click" Tag="2,7" Background="#666"/> <Button x:Name="Cell_2_7" Click="CellClick" Tag="2,7"/>
<Button x:Name="Cell_3_0" Click="Cell_Click" Tag="3,0" Background="#666"/> <Button x:Name="Cell_3_0" Click="CellClick" Tag="3,0"/>
<Button x:Name="Cell_3_1" Click="Cell_Click" Tag="3,1" Background="#EEE"/> <Button x:Name="Cell_3_1" Click="CellClick" Tag="3,1"/>
<Button x:Name="Cell_3_2" Click="Cell_Click" Tag="3,2" Background="#666"/> <Button x:Name="Cell_3_2" Click="CellClick" Tag="3,2"/>
<Button x:Name="Cell_3_3" Click="Cell_Click" Tag="3,3" Background="#EEE"/> <Button x:Name="Cell_3_3" Click="CellClick" Tag="3,3"/>
<Button x:Name="Cell_3_4" Click="Cell_Click" Tag="3,4" Background="#666"/> <Button x:Name="Cell_3_4" Click="CellClick" Tag="3,4"/>
<Button x:Name="Cell_3_5" Click="Cell_Click" Tag="3,5" Background="#EEE"/> <Button x:Name="Cell_3_5" Click="CellClick" Tag="3,5"/>
<Button x:Name="Cell_3_6" Click="Cell_Click" Tag="3,6" Background="#666"/> <Button x:Name="Cell_3_6" Click="CellClick" Tag="3,6"/>
<Button x:Name="Cell_3_7" Click="Cell_Click" Tag="3,7" Background="#EEE"/> <Button x:Name="Cell_3_7" Click="CellClick" Tag="3,7"/>
<Button x:Name="Cell_4_0" Click="Cell_Click" Tag="4,0" Background="#EEE"/> <Button x:Name="Cell_4_0" Click="CellClick" Tag="4,0"/>
<Button x:Name="Cell_4_1" Click="Cell_Click" Tag="4,1" Background="#666"/> <Button x:Name="Cell_4_1" Click="CellClick" Tag="4,1"/>
<Button x:Name="Cell_4_2" Click="Cell_Click" Tag="4,2" Background="#EEE"/> <Button x:Name="Cell_4_2" Click="CellClick" Tag="4,2"/>
<Button x:Name="Cell_4_3" Click="Cell_Click" Tag="4,3" Background="#666"/> <Button x:Name="Cell_4_3" Click="CellClick" Tag="4,3"/>
<Button x:Name="Cell_4_4" Click="Cell_Click" Tag="4,4" Background="#EEE"/> <Button x:Name="Cell_4_4" Click="CellClick" Tag="4,4"/>
<Button x:Name="Cell_4_5" Click="Cell_Click" Tag="4,5" Background="#666"/> <Button x:Name="Cell_4_5" Click="CellClick" Tag="4,5"/>
<Button x:Name="Cell_4_6" Click="Cell_Click" Tag="4,6" Background="#EEE"/> <Button x:Name="Cell_4_6" Click="CellClick" Tag="4,6"/>
<Button x:Name="Cell_4_7" Click="Cell_Click" Tag="4,7" Background="#666"/> <Button x:Name="Cell_4_7" Click="CellClick" Tag="4,7"/>
<Button x:Name="Cell_5_0" Click="Cell_Click" Tag="5,0" Background="#666"/> <Button x:Name="Cell_5_0" Click="CellClick" Tag="5,0"/>
<Button x:Name="Cell_5_1" Click="Cell_Click" Tag="5,1" Background="#EEE"/> <Button x:Name="Cell_5_1" Click="CellClick" Tag="5,1"/>
<Button x:Name="Cell_5_2" Click="Cell_Click" Tag="5,2" Background="#666"/> <Button x:Name="Cell_5_2" Click="CellClick" Tag="5,2"/>
<Button x:Name="Cell_5_3" Click="Cell_Click" Tag="5,3" Background="#EEE"/> <Button x:Name="Cell_5_3" Click="CellClick" Tag="5,3"/>
<Button x:Name="Cell_5_4" Click="Cell_Click" Tag="5,4" Background="#666"/> <Button x:Name="Cell_5_4" Click="CellClick" Tag="5,4"/>
<Button x:Name="Cell_5_5" Click="Cell_Click" Tag="5,5" Background="#EEE"/> <Button x:Name="Cell_5_5" Click="CellClick" Tag="5,5"/>
<Button x:Name="Cell_5_6" Click="Cell_Click" Tag="5,6" Background="#666"/> <Button x:Name="Cell_5_6" Click="CellClick" Tag="5,6"/>
<Button x:Name="Cell_5_7" Click="Cell_Click" Tag="5,7" Background="#EEE"/> <Button x:Name="Cell_5_7" Click="CellClick" Tag="5,7"/>
<Button x:Name="Cell_6_0" Click="Cell_Click" Tag="6,0" Background="#EEE"/> <Button x:Name="Cell_6_0" Click="CellClick" Tag="6,0"/>
<Button x:Name="Cell_6_1" Click="Cell_Click" Tag="6,1" Background="#666"/> <Button x:Name="Cell_6_1" Click="CellClick" Tag="6,1"/>
<Button x:Name="Cell_6_2" Click="Cell_Click" Tag="6,2" Background="#EEE"/> <Button x:Name="Cell_6_2" Click="CellClick" Tag="6,2"/>
<Button x:Name="Cell_6_3" Click="Cell_Click" Tag="6,3" Background="#666"/> <Button x:Name="Cell_6_3" Click="CellClick" Tag="6,3"/>
<Button x:Name="Cell_6_4" Click="Cell_Click" Tag="6,4" Background="#EEE"/> <Button x:Name="Cell_6_4" Click="CellClick" Tag="6,4"/>
<Button x:Name="Cell_6_5" Click="Cell_Click" Tag="6,5" Background="#666"/> <Button x:Name="Cell_6_5" Click="CellClick" Tag="6,5"/>
<Button x:Name="Cell_6_6" Click="Cell_Click" Tag="6,6" Background="#EEE"/> <Button x:Name="Cell_6_6" Click="CellClick" Tag="6,6"/>
<Button x:Name="Cell_6_7" Click="Cell_Click" Tag="6,7" Background="#666"/> <Button x:Name="Cell_6_7" Click="CellClick" Tag="6,7"/>
<Button x:Name="Cell_7_0" Click="Cell_Click" Tag="7,0" Background="#666"/> <Button x:Name="Cell_7_0" Click="CellClick" Tag="7,0"/>
<Button x:Name="Cell_7_1" Click="Cell_Click" Tag="7,1" Background="#EEE"/> <Button x:Name="Cell_7_1" Click="CellClick" Tag="7,1"/>
<Button x:Name="Cell_7_2" Click="Cell_Click" Tag="7,2" Background="#666"/> <Button x:Name="Cell_7_2" Click="CellClick" Tag="7,2"/>
<Button x:Name="Cell_7_3" Click="Cell_Click" Tag="7,3" Background="#EEE"/> <Button x:Name="Cell_7_3" Click="CellClick" Tag="7,3"/>
<Button x:Name="Cell_7_4" Click="Cell_Click" Tag="7,4" Background="#666"/> <Button x:Name="Cell_7_4" Click="CellClick" Tag="7,4"/>
<Button x:Name="Cell_7_5" Click="Cell_Click" Tag="7,5" Background="#EEE"/> <Button x:Name="Cell_7_5" Click="CellClick" Tag="7,5"/>
<Button x:Name="Cell_7_6" Click="Cell_Click" Tag="7,6" Background="#666"/> <Button x:Name="Cell_7_6" Click="CellClick" Tag="7,6"/>
<Button x:Name="Cell_7_7" Click="Cell_Click" Tag="7,7" Background="#EEE"/> <Button x:Name="Cell_7_7" Click="CellClick" Tag="7,7"/>
</UniformGrid> </UniformGrid>
</Border> </Border>
<!-- Bottom Status -->
<Border Grid.Row="2" <Border Grid.Row="2"
Background="#333" Background="#333"
CornerRadius="10" CornerRadius="10"
Padding="10" Padding="10"
Margin="0,15,0,0"> Margin="0,15,0,0">
<TextBlock x:Name="StatusText" <TextBlock x:Name="StatusText"
Text="Red player's turn" Text="Red player's turn"
Foreground="White" Foreground="White"
FontSize="18" FontSize="18"
FontWeight="SemiBold" FontWeight="SemiBold"
HorizontalAlignment="Center"/> HorizontalAlignment="Center"/>
</Border> </Border>
</Grid> </Grid>
</Window> </Window>
+255 -1
View File
@@ -1,12 +1,266 @@
using System.Windows; using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace CheckersSpielBot namespace CheckersSpielBot
{ {
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly GameService _game;
private readonly BoardRenderer _renderer;
private readonly DatabaseService _db = new();
private AiService _ai;
private int _humanPlayer = 1;
private int _aiPlayer = 2;
private bool _gameEnded = false;
private int _selectedRow = -1;
private int _selectedCol = -1;
private bool _hasPieceSelected = false;
private bool _isAiThinking = false;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
_game = new GameService();
_renderer = new BoardRenderer(this, _game);
_ai = new AiService(_aiPlayer);
InitializeNewGame();
} }
#region Setup
private void InitializeNewGame()
{
// End previous game as draw if it wasn't finished
if (PlayerSession.CurrentGameId != -1 && !_gameEnded)
_db.EndGame(PlayerSession.CurrentGameId, "draw");
_gameEnded = false;
GameModeDialog dialog = new GameModeDialog();
dialog.ShowDialog();
if (dialog.HumanGoesFirst)
{
_humanPlayer = 1;
_aiPlayer = 2;
}
else
{
_humanPlayer = 2;
_aiPlayer = 1;
}
_ai = new AiService(_aiPlayer);
string wentFirst = dialog.HumanGoesFirst ? "player" : "bot";
PlayerSession.CurrentGameId = _db.StartGame(PlayerSession.PlayerId, wentFirst);
_game.InitializeNewGame();
ClearSelection();
RenderBoard();
RefreshStatusBar();
if (_game.ActivePlayer == _aiPlayer) { TriggerAiTurnAsync(); }
}
#endregion
#region Input
private void CellClick(object sender, RoutedEventArgs e)
{
if (_isAiThinking) return;
if (_game.ActivePlayer != _humanPlayer) return;
if (sender is not Button btn || btn.Tag == null) return;
ParseCellTag(btn.Tag.ToString()!, out int row, out int col);
if (_game.IsChainJumpActive) { HandleChainJumpClick(row, col); return; }
if (_hasPieceSelected) HandleSecondClick(row, col);
else HandleFirstClick(row, col);
}
private void HandleFirstClick(int row, int col)
{
int piece = _game.Board[row, col];
if (!_game.BelongsToActivePlayer(piece)) return;
if (_game.MandatoryCapturePieces.Count > 0 && !_game.MandatoryCapturePieces.Contains((row, col))) return;
if (_game.GetLegalMoves(row, col, capturesOnly: false).Count == 0) return;
_selectedRow = row;
_selectedCol = col;
_hasPieceSelected = true;
RenderBoard();
}
private void HandleSecondClick(int row, int col)
{
int piece = _game.Board[row, col];
if (_game.BelongsToActivePlayer(piece))
{
bool blockedByCapture = _game.MandatoryCapturePieces.Count > 0
&& !_game.MandatoryCapturePieces.Contains((row, col));
if (!blockedByCapture && _game.GetLegalMoves(row, col, capturesOnly: false).Count > 0)
{
_selectedRow = row;
_selectedCol = col;
RenderBoard();
}
return;
}
var legalMoves = _game.GetLegalMoves(_selectedRow, _selectedCol, capturesOnly: false);
var chosen = legalMoves.Find(m => m.DestinationRow == row && m.DestinationCol == col);
if (chosen != null) { ProcessMoveResult(_game.ExecuteMove(_selectedRow, _selectedCol, chosen)); }
}
private void HandleChainJumpClick(int row, int col)
{
if (row == _game.ChainJumpRow && col == _game.ChainJumpCol) return;
var jumps = _game.GetLegalMoves(_game.ChainJumpRow, _game.ChainJumpCol, capturesOnly: true);
var chosen = jumps.Find(m => m.DestinationRow == row && m.DestinationCol == col);
if (chosen != null) { ProcessMoveResult(_game.ExecuteMove(_game.ChainJumpRow, _game.ChainJumpCol, chosen)); }
}
#endregion
#region MoveResult
private void ProcessMoveResult(MoveResult result)
{
ClearSelection();
switch (result.Type)
{
case MoveResult.ResultType.ChainJumpRequired:
if (_game.ActivePlayer == _aiPlayer)
{
TriggerAiTurnAsync();
return;
}
_hasPieceSelected = true;
_selectedRow = _game.ChainJumpRow;
_selectedCol = _game.ChainJumpCol;
RenderBoard();
StatusText.Text = "Chain jump — must continue!";
break;
case MoveResult.ResultType.Victory:
RenderBoard();
AnnounceWinner(result.Victor);
break;
case MoveResult.ResultType.TurnAdvanced:
RenderBoard();
RefreshStatusBar();
if (_game.ActivePlayer == _aiPlayer) { TriggerAiTurnAsync(); }
break;
}
}
#endregion
#region AI
private async void TriggerAiTurnAsync()
{
_isAiThinking = true;
StatusText.Text = "Bot is thinking...";
int[,] boardSnapshot = (int[,])_game.Board.Clone();
Move? bestMove = await Task.Run(() => _ai.GetBestMove(boardSnapshot));
_isAiThinking = false;
if (bestMove == null)
{
AnnounceWinner(_humanPlayer);
return;
}
int originRow = -1, originCol = -1;
for (int r = 0; r < 8 && originRow == -1; r++)
{
for (int c = 0; c < 8 && originRow == -1; c++)
{
if (!BoardHelper.BelongsToPlayer(_game.Board[r, c], _aiPlayer)) continue;
var moves = _game.GetLegalMoves(r, c, capturesOnly: true);
if (moves.Count == 0) { moves = _game.GetLegalMoves(r, c, capturesOnly: false); }
if (moves.Exists(m => m.DestinationRow == bestMove.DestinationRow &&
m.DestinationCol == bestMove.DestinationCol &&
m.CapturedPieceRow == bestMove.CapturedPieceRow &&
m.CapturedPieceCol == bestMove.CapturedPieceCol))
{
originRow = r;
originCol = c;
}
}
}
if (originRow == -1) return;
ProcessMoveResult(_game.ExecuteMove(originRow, originCol, bestMove));
}
#endregion
#region UI
private void RenderBoard()
{
_renderer.RenderBoard(_selectedRow, _selectedCol, _hasPieceSelected, _game.IsChainJumpActive);
}
private void ClearSelection()
{
_hasPieceSelected = false;
_selectedRow = -1;
_selectedCol = -1;
}
private void RefreshStatusBar()
{
if (_game.ActivePlayer == _aiPlayer)
{
StatusText.Text = "Bot's turn";
return;
}
string hint = _game.MandatoryCapturePieces.Count > 0 ? " — Capture is mandatory!" : "";
StatusText.Text = $"Your turn{hint}";
}
private void AnnounceWinner(int victor)
{
_gameEnded = true;
string winnerStr = victor == _humanPlayer ? "player" : "bot";
_db.EndGame(PlayerSession.CurrentGameId, winnerStr);
string message = victor == _humanPlayer ? "You win!" : "Bot wins!";
StatusText.Text = message;
MessageBox.Show(message, "Game Over", MessageBoxButton.OK, MessageBoxImage.None);
}
private void History_Click(object sender, RoutedEventArgs e)
{
var dialog = new HistoryDialog();
dialog.ShowDialog();
}
private void Window_Closing(object sender, CancelEventArgs e)
{
if (PlayerSession.CurrentGameId != -1 && !_gameEnded)
_db.EndGame(PlayerSession.CurrentGameId, "draw");
}
private static void ParseCellTag(string tag, out int row, out int col)
{
var parts = tag.Split(',');
row = int.Parse(parts[0]);
col = int.Parse(parts[1]);
}
private void NewGame(object sender, RoutedEventArgs e) { InitializeNewGame(); }
#endregion
} }
} }
+20
View File
@@ -0,0 +1,20 @@
namespace CheckersSpielBot
{
public class Move
{
public int DestinationRow { get; }
public int DestinationCol { get; }
public int CapturedPieceRow { get; }
public int CapturedPieceCol { get; }
public bool IsCapture => CapturedPieceRow >= 0;
public Move(int destinationRow, int destinationCol,
int capturedPieceRow, int capturedPieceCol)
{
DestinationRow = destinationRow;
DestinationCol = destinationCol;
CapturedPieceRow = capturedPieceRow;
CapturedPieceCol = capturedPieceCol;
}
}
}
+58
View File
@@ -0,0 +1,58 @@
namespace CheckersSpielBot
{
public class MoveGeneratorService
{
private readonly int[,] _board;
public MoveGeneratorService(int[,] board)
{
_board = board;
}
public List<Move> GetLegalMoves(int row, int col, bool capturesOnly)
{
var moves = new List<Move>();
int piece = _board[row, col];
if (piece == 0) return moves;
var dirs = ResolveDirections(piece);
// Captures first
foreach (var (dr, dc) in dirs)
{
int mr = row + dr, mc = col + dc;
int lr = row + 2 * dr, lc = col + 2 * dc;
if (!BoardHelper.IsWithinBounds(mr, mc) || !BoardHelper.IsWithinBounds(lr, lc))
continue;
int mid = _board[mr, mc];
if (mid != 0 && BoardHelper.IsOpponentPiece(piece, mid) && _board[lr, lc] == 0)
moves.Add(new Move(lr, lc, mr, mc));
}
if (capturesOnly || moves.Count > 0) return moves;
// Normal moves
foreach (var (dr, dc) in dirs)
{
int lr = row + dr, lc = col + dc;
if (BoardHelper.IsWithinBounds(lr, lc) && _board[lr, lc] == 0)
moves.Add(new Move(lr, lc, -1, -1));
}
return moves;
}
private static List<(int dr, int dc)> ResolveDirections(int piece)
{
bool isKing = BoardHelper.IsKing(piece);
bool isRedPiece = BoardHelper.IsRedPiece(piece);
var dirs = new List<(int, int)>();
if (isRedPiece || isKing) { dirs.Add((-1, -1)); dirs.Add((-1, +1)); }
if (!isRedPiece || isKing) { dirs.Add((+1, -1)); dirs.Add((+1, +1)); }
return dirs;
}
}
}
+21
View File
@@ -0,0 +1,21 @@
namespace CheckersSpielBot
{
public class MoveResult
{
public enum ResultType { TurnAdvanced, ChainJumpRequired, Victory }
public ResultType Type { get; private set; }
public int Victor { get; private set; }
private MoveResult() { }
public static readonly MoveResult TurnAdvanced = new() { Type = ResultType.TurnAdvanced };
public static readonly MoveResult ChainJumpRequired = new() { Type = ResultType.ChainJumpRequired };
public static MoveResult Victory(int victor) => new()
{
Type = ResultType.Victory,
Victor = victor
};
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace CheckersSpielBot
{
public static class PlayerSession
{
public static int PlayerId { get; set; } = -1;
public static string Username { get; set; } = string.Empty;
public static int CurrentGameId { get; set; } = -1;
}
}