logic
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
|
||||
namespace CheckersSpielBot
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -8,4 +8,14 @@
|
||||
<UseWPF>true</UseWPF>
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,13 @@
|
||||
<Window x:Class="CheckersGame.MainWindow"
|
||||
|
||||
<Window x:Class="CheckersSpielBot.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
|
||||
mc:Ignorable="d"
|
||||
|
||||
Title="Checkers Game" Height="750" Width="650"
|
||||
|
||||
WindowStartupLocation="CenterScreen"
|
||||
|
||||
Background="#222">
|
||||
Background="#222"
|
||||
Closing="Window_Closing">
|
||||
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -23,162 +16,148 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Top Panel -->
|
||||
<Border Grid.Row="0"
|
||||
|
||||
Background="#333"
|
||||
|
||||
CornerRadius="10"
|
||||
|
||||
Padding="12"
|
||||
|
||||
Margin="0,0,0,15">
|
||||
<DockPanel>
|
||||
<TextBlock Text="Checkers"
|
||||
|
||||
Foreground="White"
|
||||
|
||||
FontSize="28"
|
||||
|
||||
FontWeight="Bold"
|
||||
|
||||
DockPanel.Dock="Left"
|
||||
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
|
||||
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"
|
||||
|
||||
Width="100"
|
||||
|
||||
Height="35"
|
||||
|
||||
Margin="5"
|
||||
Click="NewGame"/>
|
||||
|
||||
Click="NewGame_Click"/>
|
||||
<Button Content="Reset"
|
||||
|
||||
Width="100"
|
||||
|
||||
Height="35"
|
||||
|
||||
Margin="5"
|
||||
|
||||
Click="Reset_Click"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1"
|
||||
|
||||
Background="#111"
|
||||
|
||||
CornerRadius="12"
|
||||
|
||||
Padding="10">
|
||||
<UniformGrid Rows="8" Columns="8">
|
||||
|
||||
<Button x:Name="Cell_0_0" Click="Cell_Click" Tag="0,0" Background="#EEE"/>
|
||||
<Button x:Name="Cell_0_1" Click="Cell_Click" Tag="0,1" Background="#666"/>
|
||||
<Button x:Name="Cell_0_2" Click="Cell_Click" Tag="0,2" Background="#EEE"/>
|
||||
<Button x:Name="Cell_0_3" Click="Cell_Click" Tag="0,3" Background="#666"/>
|
||||
<Button x:Name="Cell_0_4" Click="Cell_Click" Tag="0,4" Background="#EEE"/>
|
||||
<Button x:Name="Cell_0_5" Click="Cell_Click" Tag="0,5" Background="#666"/>
|
||||
<Button x:Name="Cell_0_6" Click="Cell_Click" Tag="0,6" Background="#EEE"/>
|
||||
<Button x:Name="Cell_0_7" Click="Cell_Click" Tag="0,7" Background="#666"/>
|
||||
<Button x:Name="Cell_0_0" Click="CellClick" Tag="0,0"/>
|
||||
<Button x:Name="Cell_0_1" Click="CellClick" Tag="0,1"/>
|
||||
<Button x:Name="Cell_0_2" Click="CellClick" Tag="0,2"/>
|
||||
<Button x:Name="Cell_0_3" Click="CellClick" Tag="0,3"/>
|
||||
<Button x:Name="Cell_0_4" Click="CellClick" Tag="0,4"/>
|
||||
<Button x:Name="Cell_0_5" Click="CellClick" Tag="0,5"/>
|
||||
<Button x:Name="Cell_0_6" Click="CellClick" Tag="0,6"/>
|
||||
<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_1" Click="Cell_Click" Tag="1,1" Background="#EEE"/>
|
||||
<Button x:Name="Cell_1_2" Click="Cell_Click" Tag="1,2" Background="#666"/>
|
||||
<Button x:Name="Cell_1_3" Click="Cell_Click" Tag="1,3" Background="#EEE"/>
|
||||
<Button x:Name="Cell_1_4" Click="Cell_Click" Tag="1,4" Background="#666"/>
|
||||
<Button x:Name="Cell_1_5" Click="Cell_Click" Tag="1,5" Background="#EEE"/>
|
||||
<Button x:Name="Cell_1_6" Click="Cell_Click" Tag="1,6" Background="#666"/>
|
||||
<Button x:Name="Cell_1_7" Click="Cell_Click" Tag="1,7" Background="#EEE"/>
|
||||
<Button x:Name="Cell_1_0" Click="CellClick" Tag="1,0"/>
|
||||
<Button x:Name="Cell_1_1" Click="CellClick" Tag="1,1"/>
|
||||
<Button x:Name="Cell_1_2" Click="CellClick" Tag="1,2"/>
|
||||
<Button x:Name="Cell_1_3" Click="CellClick" Tag="1,3"/>
|
||||
<Button x:Name="Cell_1_4" Click="CellClick" Tag="1,4"/>
|
||||
<Button x:Name="Cell_1_5" Click="CellClick" Tag="1,5"/>
|
||||
<Button x:Name="Cell_1_6" Click="CellClick" Tag="1,6"/>
|
||||
<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_1" Click="Cell_Click" Tag="2,1" Background="#666"/>
|
||||
<Button x:Name="Cell_2_2" Click="Cell_Click" Tag="2,2" Background="#EEE"/>
|
||||
<Button x:Name="Cell_2_3" Click="Cell_Click" Tag="2,3" Background="#666"/>
|
||||
<Button x:Name="Cell_2_4" Click="Cell_Click" Tag="2,4" Background="#EEE"/>
|
||||
<Button x:Name="Cell_2_5" Click="Cell_Click" Tag="2,5" Background="#666"/>
|
||||
<Button x:Name="Cell_2_6" Click="Cell_Click" Tag="2,6" Background="#EEE"/>
|
||||
<Button x:Name="Cell_2_7" Click="Cell_Click" Tag="2,7" Background="#666"/>
|
||||
<Button x:Name="Cell_2_0" Click="CellClick" Tag="2,0"/>
|
||||
<Button x:Name="Cell_2_1" Click="CellClick" Tag="2,1"/>
|
||||
<Button x:Name="Cell_2_2" Click="CellClick" Tag="2,2"/>
|
||||
<Button x:Name="Cell_2_3" Click="CellClick" Tag="2,3"/>
|
||||
<Button x:Name="Cell_2_4" Click="CellClick" Tag="2,4"/>
|
||||
<Button x:Name="Cell_2_5" Click="CellClick" Tag="2,5"/>
|
||||
<Button x:Name="Cell_2_6" Click="CellClick" Tag="2,6"/>
|
||||
<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_1" Click="Cell_Click" Tag="3,1" Background="#EEE"/>
|
||||
<Button x:Name="Cell_3_2" Click="Cell_Click" Tag="3,2" Background="#666"/>
|
||||
<Button x:Name="Cell_3_3" Click="Cell_Click" Tag="3,3" Background="#EEE"/>
|
||||
<Button x:Name="Cell_3_4" Click="Cell_Click" Tag="3,4" Background="#666"/>
|
||||
<Button x:Name="Cell_3_5" Click="Cell_Click" Tag="3,5" Background="#EEE"/>
|
||||
<Button x:Name="Cell_3_6" Click="Cell_Click" Tag="3,6" Background="#666"/>
|
||||
<Button x:Name="Cell_3_7" Click="Cell_Click" Tag="3,7" Background="#EEE"/>
|
||||
<Button x:Name="Cell_3_0" Click="CellClick" Tag="3,0"/>
|
||||
<Button x:Name="Cell_3_1" Click="CellClick" Tag="3,1"/>
|
||||
<Button x:Name="Cell_3_2" Click="CellClick" Tag="3,2"/>
|
||||
<Button x:Name="Cell_3_3" Click="CellClick" Tag="3,3"/>
|
||||
<Button x:Name="Cell_3_4" Click="CellClick" Tag="3,4"/>
|
||||
<Button x:Name="Cell_3_5" Click="CellClick" Tag="3,5"/>
|
||||
<Button x:Name="Cell_3_6" Click="CellClick" Tag="3,6"/>
|
||||
<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_1" Click="Cell_Click" Tag="4,1" Background="#666"/>
|
||||
<Button x:Name="Cell_4_2" Click="Cell_Click" Tag="4,2" Background="#EEE"/>
|
||||
<Button x:Name="Cell_4_3" Click="Cell_Click" Tag="4,3" Background="#666"/>
|
||||
<Button x:Name="Cell_4_4" Click="Cell_Click" Tag="4,4" Background="#EEE"/>
|
||||
<Button x:Name="Cell_4_5" Click="Cell_Click" Tag="4,5" Background="#666"/>
|
||||
<Button x:Name="Cell_4_6" Click="Cell_Click" Tag="4,6" Background="#EEE"/>
|
||||
<Button x:Name="Cell_4_7" Click="Cell_Click" Tag="4,7" Background="#666"/>
|
||||
<Button x:Name="Cell_4_0" Click="CellClick" Tag="4,0"/>
|
||||
<Button x:Name="Cell_4_1" Click="CellClick" Tag="4,1"/>
|
||||
<Button x:Name="Cell_4_2" Click="CellClick" Tag="4,2"/>
|
||||
<Button x:Name="Cell_4_3" Click="CellClick" Tag="4,3"/>
|
||||
<Button x:Name="Cell_4_4" Click="CellClick" Tag="4,4"/>
|
||||
<Button x:Name="Cell_4_5" Click="CellClick" Tag="4,5"/>
|
||||
<Button x:Name="Cell_4_6" Click="CellClick" Tag="4,6"/>
|
||||
<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_1" Click="Cell_Click" Tag="5,1" Background="#EEE"/>
|
||||
<Button x:Name="Cell_5_2" Click="Cell_Click" Tag="5,2" Background="#666"/>
|
||||
<Button x:Name="Cell_5_3" Click="Cell_Click" Tag="5,3" Background="#EEE"/>
|
||||
<Button x:Name="Cell_5_4" Click="Cell_Click" Tag="5,4" Background="#666"/>
|
||||
<Button x:Name="Cell_5_5" Click="Cell_Click" Tag="5,5" Background="#EEE"/>
|
||||
<Button x:Name="Cell_5_6" Click="Cell_Click" Tag="5,6" Background="#666"/>
|
||||
<Button x:Name="Cell_5_7" Click="Cell_Click" Tag="5,7" Background="#EEE"/>
|
||||
<Button x:Name="Cell_5_0" Click="CellClick" Tag="5,0"/>
|
||||
<Button x:Name="Cell_5_1" Click="CellClick" Tag="5,1"/>
|
||||
<Button x:Name="Cell_5_2" Click="CellClick" Tag="5,2"/>
|
||||
<Button x:Name="Cell_5_3" Click="CellClick" Tag="5,3"/>
|
||||
<Button x:Name="Cell_5_4" Click="CellClick" Tag="5,4"/>
|
||||
<Button x:Name="Cell_5_5" Click="CellClick" Tag="5,5"/>
|
||||
<Button x:Name="Cell_5_6" Click="CellClick" Tag="5,6"/>
|
||||
<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_1" Click="Cell_Click" Tag="6,1" Background="#666"/>
|
||||
<Button x:Name="Cell_6_2" Click="Cell_Click" Tag="6,2" Background="#EEE"/>
|
||||
<Button x:Name="Cell_6_3" Click="Cell_Click" Tag="6,3" Background="#666"/>
|
||||
<Button x:Name="Cell_6_4" Click="Cell_Click" Tag="6,4" Background="#EEE"/>
|
||||
<Button x:Name="Cell_6_5" Click="Cell_Click" Tag="6,5" Background="#666"/>
|
||||
<Button x:Name="Cell_6_6" Click="Cell_Click" Tag="6,6" Background="#EEE"/>
|
||||
<Button x:Name="Cell_6_7" Click="Cell_Click" Tag="6,7" Background="#666"/>
|
||||
<Button x:Name="Cell_6_0" Click="CellClick" Tag="6,0"/>
|
||||
<Button x:Name="Cell_6_1" Click="CellClick" Tag="6,1"/>
|
||||
<Button x:Name="Cell_6_2" Click="CellClick" Tag="6,2"/>
|
||||
<Button x:Name="Cell_6_3" Click="CellClick" Tag="6,3"/>
|
||||
<Button x:Name="Cell_6_4" Click="CellClick" Tag="6,4"/>
|
||||
<Button x:Name="Cell_6_5" Click="CellClick" Tag="6,5"/>
|
||||
<Button x:Name="Cell_6_6" Click="CellClick" Tag="6,6"/>
|
||||
<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_1" Click="Cell_Click" Tag="7,1" Background="#EEE"/>
|
||||
<Button x:Name="Cell_7_2" Click="Cell_Click" Tag="7,2" Background="#666"/>
|
||||
<Button x:Name="Cell_7_3" Click="Cell_Click" Tag="7,3" Background="#EEE"/>
|
||||
<Button x:Name="Cell_7_4" Click="Cell_Click" Tag="7,4" Background="#666"/>
|
||||
<Button x:Name="Cell_7_5" Click="Cell_Click" Tag="7,5" Background="#EEE"/>
|
||||
<Button x:Name="Cell_7_6" Click="Cell_Click" Tag="7,6" Background="#666"/>
|
||||
<Button x:Name="Cell_7_7" Click="Cell_Click" Tag="7,7" Background="#EEE"/>
|
||||
<Button x:Name="Cell_7_0" Click="CellClick" Tag="7,0"/>
|
||||
<Button x:Name="Cell_7_1" Click="CellClick" Tag="7,1"/>
|
||||
<Button x:Name="Cell_7_2" Click="CellClick" Tag="7,2"/>
|
||||
<Button x:Name="Cell_7_3" Click="CellClick" Tag="7,3"/>
|
||||
<Button x:Name="Cell_7_4" Click="CellClick" Tag="7,4"/>
|
||||
<Button x:Name="Cell_7_5" Click="CellClick" Tag="7,5"/>
|
||||
<Button x:Name="Cell_7_6" Click="CellClick" Tag="7,6"/>
|
||||
<Button x:Name="Cell_7_7" Click="CellClick" Tag="7,7"/>
|
||||
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
|
||||
<!-- Bottom Status -->
|
||||
<Border Grid.Row="2"
|
||||
|
||||
Background="#333"
|
||||
|
||||
CornerRadius="10"
|
||||
|
||||
Padding="10"
|
||||
|
||||
Margin="0,15,0,0">
|
||||
<TextBlock x:Name="StatusText"
|
||||
|
||||
Text="Red player's turn"
|
||||
|
||||
Foreground="White"
|
||||
|
||||
FontSize="18"
|
||||
|
||||
FontWeight="SemiBold"
|
||||
|
||||
HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,12 +1,266 @@
|
||||
using System.Windows;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace CheckersSpielBot
|
||||
{
|
||||
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()
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user