Add chess (Oh my gosh big commit)

This commit is contained in:
jheaps
2025-03-27 13:54:07 -06:00
parent 2edc40fb99
commit fbed5ae789
41 changed files with 1943 additions and 71 deletions
+6
View File
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.9.34728.123
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net", "JoshHeaps.Net\JoshHeaps.Net.csproj", "{9F0182CC-470F-4D1A-99F5-348D7921751E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net", "JoshHeaps.Net\JoshHeaps.Net.csproj", "{9F0182CC-470F-4D1A-99F5-348D7921751E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{82206AD3-19DF-4DDA-9647-02B691B78CE8}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F0182CC-470F-4D1A-99F5-348D7921751E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.Build.0 = Release|Any CPU {9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.Build.0 = Release|Any CPU
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -0,0 +1,192 @@
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Concurrent;
namespace JoshHeaps.Net.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ChessController : ControllerBase
{
private readonly IChessService chessService;
/// <summary>
/// Store of ongoing games.
/// </summary>
private static readonly ConcurrentDictionary<Guid, GameState> _games = [];
private static ConcurrentDictionary<Guid, Task> _gameRemovalTasks = [];
public ChessController(IChessService chessService)
{
this.chessService = chessService;
}
/// <summary>
/// Create a new chess game and store it in-memory.
/// </summary>
[HttpPost("new")]
public ActionResult CreateGame()
{
var gameState = chessService.CreateNewGame();
_games[gameState.GameId] = gameState;
return Ok(new { gameState.GameId });
}
/// <summary>
/// Joins the "pool" of chess players.
/// Test code expects to receive a GUID for the player
/// and a bool indicating if they are White.
/// </summary>
[HttpGet("JoinGame")]
public ActionResult JoinGame()
{
Console.WriteLine("joining game");
GameState? gameState = _games.Values.FirstOrDefault(g => g.IsOpen);
if (gameState == null)
{
gameState = chessService.CreateNewGame();
_games[gameState.GameId] = gameState;
}
Guid playerId = Guid.NewGuid();
bool isWhite = true;
if (!gameState.WhiteJoined)
{
gameState.WhiteJoined = true;
gameState.WhitePlayerId = playerId;
}
else if (!gameState.BlackJoined)
{
gameState.BlackJoined = true;
gameState.BlackPlayerId = playerId;
isWhite = false;
}
return Ok(new
{
Id = playerId,
IsWhite = isWhite,
gameState.GameId
});
}
/// <summary>
/// Get the state of an existing game by ID.
/// </summary>
[HttpGet("{gameId}")]
public ActionResult GetGameState(Guid gameId)
{
if (!_games.TryGetValue(gameId, out var gameState))
return NotFound("Game not found");
var response = new
{
gameState.GameId,
CurrentPlayer = gameState.CurrentPlayer.ToString(),
gameState.IsCheck,
gameState.IsCheckmate,
gameState.IsStalemate,
EnPassantTarget = gameState.EnPassantTarget?.ToString() ?? null,
gameState.WhiteCanCastleKingside,
gameState.WhiteCanCastleQueenside,
gameState.BlackCanCastleKingside,
gameState.BlackCanCastleQueenside,
Pieces = gameState.Pieces
.Where(p => p.Position.Row >= 0)
.Select(p => new {
p.Id,
p.Type,
p.Color,
p.Position.Row,
p.Position.Col,
p.HasMoved
}),
gameState.MoveHistory
};
return Ok(response);
}
/// <summary>
/// Make a move in the specified game.
/// The test passes a JSON body with a MoveDto.
/// </summary>
[HttpPost("move")]
public ActionResult MakeMove([FromBody] MoveDto moveDto)
{
if (!_games.TryGetValue(moveDto.GameId, out var gameState))
return NotFound("Game not found");
// Check if player is authorized to move
var isWhiteMove = gameState.CurrentPlayer == PieceColor.White;
var expectedPlayerId = isWhiteMove ? gameState.WhitePlayerId : gameState.BlackPlayerId;
if (moveDto.PlayerId != expectedPlayerId)
return Forbid("You are not the current player.");
// Make sure player owns the piece
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId);
if (piece is null)
return NotFound("Chess piece Id does not exist");
if ((isWhiteMove && piece.Color != PieceColor.White) || (!isWhiteMove && piece?.Color != PieceColor.Black))
return Forbid("You cannot move this piece.");
var result = chessService.MakeMove(gameState, moveDto);
if (!result.Success)
return BadRequest(result);
if (result.IsCheckmate || result.IsStalemate)
{
// queue game removal
_gameRemovalTasks.TryAdd(moveDto.GameId, Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(1));
_games.Remove(moveDto.GameId, out _);
_gameRemovalTasks.Remove(moveDto.GameId, out _);
}));
}
return Ok(result);
}
/// <summary>
/// Get the legal moves for a specific piece in a specific game.
/// </summary>
[HttpGet("{gameId}/legalMoves/{pieceId}")]
public ActionResult GetLegalMoves(Guid gameId, string pieceId)
{
if (!_games.TryGetValue(gameId, out var gameState))
return NotFound("Game not found");
var moves = chessService.GetLegalMovesForPiece(gameState, pieceId);
return Ok(moves);
}
/// <summary>
/// (Optional) Get all legal moves for the current player.
/// This was in your snippet, so we'll keep it.
/// </summary>
[HttpGet("{gameId}/legalMoves")]
public ActionResult GetAllLegalMoves(Guid gameId)
{
if (!_games.TryGetValue(gameId, out var gameState))
return NotFound("Game not found");
var allMoves = chessService.GetAllLegalMoves(gameState)
.Select(x => new
{
PieceId = x.piece.Id,
Moves = x.moves
});
return Ok(allMoves);
}
}
+18
View File
@@ -0,0 +1,18 @@
using JoshHeaps.Net.Models;
using Microsoft.AspNetCore.SignalR;
namespace JoshHeaps.Net.Hubs;
public class ChessHub : Hub
{
public async Task JoinWebsocketGroup(string gameId)
{
Console.WriteLine($"🔌 Joining group {gameId}");
await Groups.AddToGroupAsync(Context.ConnectionId, gameId);
}
public async Task MoveMade(string gameId, MoveResultDto moveResult)
{
await Clients.OthersInGroup(gameId).SendAsync("ReceiveMoveUpdate", gameId, moveResult);
}
}
+8
View File
@@ -6,4 +6,12 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\images\Chess Images\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.7" />
</ItemGroup>
</Project> </Project>
+11
View File
@@ -0,0 +1,11 @@
namespace JoshHeaps.Net.Models;
public class ChessPiece(string id, PieceType type, PieceColor color, Position position)
{
public string Id { get; set; } = id;
public PieceType Type { get; set; } = type;
public PieceColor Color { get; set; } = color;
public Position Position { get; set; } = position;
public bool HasMoved { get; set; } = false;
}
+17
View File
@@ -0,0 +1,17 @@
namespace JoshHeaps.Net.Models;
public enum PieceType
{
Pawn,
Rook,
Knight,
Bishop,
Queen,
King
}
public enum PieceColor
{
White,
Black
}
+52
View File
@@ -0,0 +1,52 @@
namespace JoshHeaps.Net.Models;
public class GameState
{
public Guid GameId { get; set; }
// 8x8 board of references. Null if no piece is present.
// row 0 at top -> row 7 at bottom (typical 0-based array).
public ChessPiece?[,] Board { get; set; }
// Whose turn is it?
public PieceColor CurrentPlayer { get; set; }
// To detect if en passant is possible, store the position of a pawn that just moved two squares.
// If no pawn is currently "en-passant capturable," this could be null.
public Position? EnPassantTarget { get; set; }
// Castling rights: have the rooks or king moved?
// In real chess notation, you track it per side, e.g. "KQkq" style. Lets store boolean flags:
public bool WhiteCanCastleKingside { get; set; }
public bool WhiteCanCastleQueenside { get; set; }
public bool BlackCanCastleKingside { get; set; }
public bool BlackCanCastleQueenside { get; set; }
// Some game status
public bool IsCheck { get; set; }
public bool IsCheckmate { get; set; }
public bool IsStalemate { get; set; }
// Keep a history of moves if desired
public List<string> MoveHistory { get; set; }
// A list of all pieces to quickly reference them (optional but convenient).
// Alternatively, you can iterate the Board array.
public List<ChessPiece> Pieces { get; set; }
public bool WhiteJoined { get; set; } = false;
public bool BlackJoined { get; set; } = false;
public Guid WhitePlayerId { get; set; }
public Guid BlackPlayerId { get; set; }
// optional: convenience
public bool IsOpen => !WhiteJoined || !BlackJoined;
public GameState()
{
GameId = Guid.NewGuid();
Board = new ChessPiece[8, 8];
Pieces = [];
MoveHistory = [];
}
}
+20
View File
@@ -0,0 +1,20 @@
namespace JoshHeaps.Net.Models;
public class MoveDto
{
public Guid GameId { get; set; }
public Guid PlayerId { get; set; }
public string PieceId { get; set; }
// The square the piece is moving from
public int SourceRow { get; set; }
public int SourceCol { get; set; }
// The square the piece is moving to
public int TargetRow { get; set; }
public int TargetCol { get; set; }
// If this is a pawn promotion, specify the piece type to promote to; otherwise null
public PieceType? PromotionChoice { get; set; }
}
+10
View File
@@ -0,0 +1,10 @@
namespace JoshHeaps.Net.Models;
public class MoveResultDto
{
public bool Success { get; set; }
public string Message { get; set; }
public bool IsCheck { get; set; }
public bool IsCheckmate { get; set; }
public bool IsStalemate { get; set; }
}
+18
View File
@@ -0,0 +1,18 @@
namespace JoshHeaps.Net.Models;
public struct Position(int row, int col)
{
public int Row { get; set; } = row;
public int Col { get; set; } = col;
public override readonly string ToString()
{
return $"[{Row}, {Col}]";
}
internal readonly void Deconstruct(out int row, out int col)
{
row = Row; col = Col;
}
}
+37
View File
@@ -0,0 +1,37 @@
@page
@model JoshHeaps.Net.Pages.ChessModel
@{
Layout = "_Layout";
ViewData["Title"] = "Chess";
}
<div id="chessContainer">
<div id="boardContainer">
<div id="chessBoard">
<!-- Placeholder squares -->
@for (int i = 0; i < 64; i++)
{
<div id="square-@i" class="chessSquare @( (i + i / 8) % 2 == 0 ? "light" : "dark" )"></div>
}
</div>
</div>
<div id="textContainer" class="sideContent">
<h1>Chess Arena</h1>
<p>This page will host your chess UI and interactions.</p>
</div>
<div id="buttonContainer" class="sideContent">
<button id="startGameBtn" onclick="startNewGame()">Start New Game</button>
</div>
</div>
@section Scripts {
<script src="~/js/signalr/signalr.min.js"></script>
<script src="~/js/ChessScripts/chessLogic.js"></script>
}
@section Styles {
<link rel="stylesheet" href="~/css/chess/game.css?v=@ViewData["cssVersion"]" />
<link rel="stylesheet" href="~/css/chess/site.css?v=@ViewData["cssVersion"]" />
}
+12
View File
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JoshHeaps.Net.Pages
{
public class ChessModel : PageModel
{
public void OnGet()
{
}
}
}
+48 -29
View File
@@ -1,30 +1,18 @@
@page @page
@model JoshHeaps.Net.Pages.IndexModel @model JoshHeaps.Net.Pages.IndexModel
@{ @{
ViewData["Title"] = "Home page"; ViewData["Title"] = "JoshHeaps.Net";
Layout = "_Layout";
} }
<html lang="en-us"> <div id="Header">
<head>
<title>JoshHeaps.Net</title>
<script src="/js/site.js"></script>
<script src="/js/utils.js"></script>
<script src="/js/opacityUpdateObserver.js"></script>
<link href="/css/site.css" rel="stylesheet" type="text/css"/>
<link href="/css/project.css" rel="stylesheet" type="text/css"/>
<link href="/css/imageLeftProject.css" rel="stylesheet" type="text/css"/>
<link href="/css/imageRightProject.css" rel="stylesheet" type="text/css"/>
<link href="/css/footer.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="Header">
<a class="headerOption" onclick="showClickedContents('projects')">./ Projects</a> <a class="headerOption" onclick="showClickedContents('projects')">./ Projects</a>
<a class="headerOption" onclick="showClickedContents('contact')">./ Contact me</a> <a class="headerOption" onclick="showClickedContents('contact')">./ Contact me</a>
</div> </div>
<div id="WelcomeMessage"></div> <div id="WelcomeMessage"></div>
<div id="ProjectsBox"> <div id="ProjectsBox">
<div id="ChessProject" class="projectContainer"> <div id="ChessProject" class="projectContainer">
<div class="displayBox diagonal-section-left"> <div class="displayBox diagonal-section-left">
<a target="_blank" href="https://github.com/JoshHeaps/OnlineChess"> <a target="_blank" href="https://github.com/JoshHeaps/OnlineChess">
@@ -52,28 +40,59 @@
</a> </a>
</div> </div>
</div> </div>
</div>
<div id="Contacts"> <div id="ChessProject" class="projectContainer">
<a id="GithubLink" class="social-link" href="https://github.com/JoshHeaps" target="_blank"> <div class="displayBox diagonal-section-left">
<a target="_blank" href="https://github.com/JoshHeaps/OnlineChess">
<img id="ChessImage" class="projectImage projectImageLeft" src="/images/Chess.jpg" title="Chessboard"/>
</a>
</div>
<div id="ChessText" class="projectTextRight">
<h2 id="ChessTitle" class="projectHeaderRight projectHeader">Chess</h2>
<p id="ChessDescription" class="projectDescriptionRight projectDescription">
As someone who loves chess, I wanted to create a chess game that I could play with my friends and family. This project is a work in progress, as currently it can only be played locally. It was built in C# on .NET 8.0, using winforms, because I like a challenge. I plan to add online multiplayer functionality in the future.
</p>
</div>
</div>
</div>
<div id="Contacts">
<svg viewBox="0 0 16 16" class="social-icon"> <svg viewBox="0 0 16 16" class="social-icon">
<a id="GithubLink" class="social-link" href="https://github.com/JoshHeaps" target="_blank">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
</svg> <rect width="100%" height="100%" fill="transparent" />
</a> </a>
<a id="LinkedinLink" class="social-link" href="https://www.linkedin.com/in/josh-heaps/" target="_blank"> </svg>
<svg viewBox="0 0 24 24" class="social-icon"> <svg viewBox="0 0 24 24" class="social-icon">
<a id="LinkedinLink" class="social-link" href="https://www.linkedin.com/in/josh-heaps/" target="_blank">
<path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19a.66.66 0 000 .14V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"></path> <path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19a.66.66 0 000 .14V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"></path>
</svg> <rect width="100%" height="100%" fill="transparent" />
</a> </a>
<a id="GmailLink" class="social-link" href="mailto:[email protected]"> </svg>
<svg viewBox="0 -4 31 31" class="social-icon" preserveAspectRatio="none"> <svg viewBox="0 -4 31 31" class="social-icon" preserveAspectRatio="none">
<a id="GmailLink" class="social-link" href="mailto:[email protected]">
<path d="m0 3.23863636v2.72727273l3.12784091 3.02727273 3.69034091 2.08636368.68181818-4.59034095-.68181818-4.27329546-1.90909091-1.43181818c-2.02329546-1.51704546-4.90909091-.07329545-4.90909091 2.45454545"/> <path d="m0 3.23863636v2.72727273l3.12784091 3.02727273 3.69034091 2.08636368.68181818-4.59034095-.68181818-4.27329546-1.90909091-1.43181818c-2.02329546-1.51704546-4.90909091-.07329545-4.90909091 2.45454545"/>
<path d="m23.1818182 2.21590909-.6818182 4.32954546.6818182 4.53409095 3.3494318-1.65852277 3.46875-3.45511364v-2.72727273c0-2.5278409-2.8857955-3.97159091-4.9090909-2.45454545z"/> <path d="m23.1818182 2.21590909-.6818182 4.32954546.6818182 4.53409095 3.3494318-1.65852277 3.46875-3.45511364v-2.72727273c0-2.5278409-2.8857955-3.97159091-4.9090909-2.45454545z"/>
<path d="m2.04545455 22.6704545h4.77272727v-11.590909l-6.81818182-5.11363641v14.65909091c0 1.1301136.91534091 2.0454545 2.04545455 2.0454545"/> <path d="m2.04545455 22.6704545h4.77272727v-11.590909l-6.81818182-5.11363641v14.65909091c0 1.1301136.91534091 2.0454545 2.04545455 2.0454545"/>
<path d="m23.1818182 22.6704545h4.7727273c1.1301136 0 2.0454545-.9153409 2.0454545-2.0454545v-14.65909091l-6.8181818 5.11363641z"/> <path d="m23.1818182 22.6704545h4.7727273c1.1301136 0 2.0454545-.9153409 2.0454545-2.0454545v-14.65909091l-6.8181818 5.11363641z"/>
<path d="m15 8.35227273-8.18181818-6.13636364v8.86363641l8.18181818 6.1363636 8.1818182-6.1363636v-8.86363641z"/> <path d="m15 8.35227273-8.18181818-6.13636364v8.86363641l8.18181818 6.1363636 8.1818182-6.1363636v-8.86363641z"/>
</svg> <rect width="100%" height="100%" fill="transparent" />
</a> </a>
</div> </svg>
</body> </div>
</html>
@section Styles {
<link href="/css/home/site.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/project.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/imageLeftProject.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/imageRightProject.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/footer.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
}
@section Scripts {
<script src="/js/site.js"></script>
<script src="/js/utils.js"></script>
<script src="/js/opacityUpdateObserver.js"></script>
}
+28
View File
@@ -0,0 +1,28 @@
@{
Layout = null;
ViewData["cssVersion"] = "1.0.0"; // <--- change this once to bust cache
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
@RenderSection("Styles", required: false)
@RenderSection("Scripts", required: false)
</head>
<body>
<header>
<!-- optional header markup -->
</header>
<main role="main">
@RenderBody()
</main>
<footer>
<!-- optional footer markup -->
</footer>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JoshHeaps.Net.Pages
{
public class _LayoutModel : PageModel
{
public void OnGet()
{
}
}
}
+24 -1
View File
@@ -1,8 +1,18 @@
using JoshHeaps.Net.Hubs;
using JoshHeaps.Net.Services.Implementations;
using JoshHeaps.Net.Services.Interfaces;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddControllers();
builder.Services.AddSignalR();
builder.Services.AddSingleton<IChessService, ChessService>();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
@@ -14,7 +24,16 @@ if (!app.Environment.IsDevelopment())
} }
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
ctx.Context.Response.Headers.Append("Pragma", "no-cache");
ctx.Context.Response.Headers.Append("Expires", "0");
}
});
app.UseRouting(); app.UseRouting();
@@ -22,4 +41,8 @@ app.UseAuthorization();
app.MapRazorPages(); app.MapRazorPages();
app.MapControllers();
app.MapHub<ChessHub>("/chessHub");
app.Run(); app.Run();
@@ -0,0 +1,697 @@
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
namespace JoshHeaps.Net.Services.Implementations;
public class ChessService : IChessService
{
public GameState CreateNewGame()
{
var gameState = new GameState();
InitializeBoard(gameState);
return gameState;
}
private void InitializeBoard(GameState gameState)
{
for (int r = 0; r < 8; r++)
for (int c = 0; c < 8; c++)
gameState.Board[r, c] = null;
gameState.Pieces.Clear();
gameState.MoveHistory.Clear();
gameState.WhiteCanCastleKingside = true;
gameState.WhiteCanCastleQueenside = true;
gameState.BlackCanCastleKingside = true;
gameState.BlackCanCastleQueenside = true;
gameState.EnPassantTarget = null;
SetupBlackPieces(gameState);
SetupWhitePieces(gameState);
gameState.CurrentPlayer = PieceColor.White;
UpdateCheckStatus(gameState);
}
private static void SetupBlackPieces(GameState gs)
{
var blackMajors = new[]
{
PieceType.Rook, PieceType.Knight, PieceType.Bishop,
PieceType.Queen, PieceType.King, PieceType.Bishop,
PieceType.Knight, PieceType.Rook
};
for (int c = 0; c < 8; c++)
{
var major = new ChessPiece($"b{blackMajors[c]}{c}", blackMajors[c], PieceColor.Black, new Position(0, c));
gs.Board[0, c] = major;
gs.Pieces.Add(major);
}
for (int c = 0; c < 8; c++)
{
var pawn = new ChessPiece($"bPawn{c}", PieceType.Pawn, PieceColor.Black, new Position(1, c));
gs.Board[1, c] = pawn;
gs.Pieces.Add(pawn);
}
}
private static void SetupWhitePieces(GameState gs)
{
var whiteMajors = new[]
{
PieceType.Rook, PieceType.Knight, PieceType.Bishop,
PieceType.Queen, PieceType.King, PieceType.Bishop,
PieceType.Knight, PieceType.Rook
};
for (int c = 0; c < 8; c++)
{
var major = new ChessPiece($"w{whiteMajors[c]}{c}", whiteMajors[c], PieceColor.White, new Position(7, c));
gs.Board[7, c] = major;
gs.Pieces.Add(major);
}
for (int c = 0; c < 8; c++)
{
var pawn = new ChessPiece($"wPawn{c}", PieceType.Pawn, PieceColor.White, new Position(6, c));
gs.Board[6, c] = pawn;
gs.Pieces.Add(pawn);
}
}
public List<(ChessPiece piece, List<Position> moves)> GetAllLegalMoves(GameState gameState)
{
var result = new List<(ChessPiece piece, List<Position> moves)>();
var currentPieces = gameState
.Pieces
.Where(p => p.Color == gameState.CurrentPlayer && p.Position.Row >= 0)
.ToList();
foreach (var piece in currentPieces)
{
var moves = GetLegalMovesForPiece(gameState, piece.Id);
if (moves.Count > 0)
result.Add((piece, moves));
}
return result;
}
public List<Position> GetLegalMovesForPiece(GameState gameState, string pieceId)
{
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == pieceId);
if (piece == null) return [];
if (piece.Color != gameState.CurrentPlayer) return [];
var candidateMoves = GenerateCandidateMoves(gameState, piece);
var legalMoves = new List<Position>();
foreach (var pos in candidateMoves)
if (IsMoveLegalConsideringCheck(gameState, piece, pos))
legalMoves.Add(pos);
return legalMoves;
}
public MoveResultDto MakeMove(GameState gameState, MoveDto moveDto)
{
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId);
if (piece == null)
return new MoveResultDto { Success = false, Message = "Piece not found." };
if (piece.Color != gameState.CurrentPlayer)
return new MoveResultDto { Success = false, Message = "Not your turn." };
var legalMoves = GetLegalMovesForPiece(gameState, piece.Id);
var targetPos = new Position(moveDto.TargetRow, moveDto.TargetCol);
if (!legalMoves.Any(m => m.Row == moveDto.TargetRow && m.Col == moveDto.TargetCol))
return new MoveResultDto { Success = false, Message = "Illegal move." };
PerformMove(gameState, piece, targetPos, moveDto);
UpdateCheckStatus(gameState);
var notation = $"{piece.Id}:{piece.Position}->{targetPos}";
gameState.MoveHistory.Add(notation);
return new MoveResultDto
{
Success = true,
Message = "Move successful.",
IsCheck = gameState.IsCheck,
IsCheckmate = gameState.IsCheckmate,
IsStalemate = gameState.IsStalemate
};
}
private static void PerformMove(GameState gs, ChessPiece piece, Position targetPos, MoveDto moveDto)
{
var oldPos = piece.Position;
var captured = gs.Board[targetPos.Row, targetPos.Col];
HandleEnPassantIfNeeded(gs, piece, targetPos, ref captured);
gs.Board[oldPos.Row, oldPos.Col] = null;
piece.Position = targetPos;
gs.Board[targetPos.Row, targetPos.Col] = piece;
if (captured != null && captured != piece)
captured.Position = new Position(-1, -1);
bool wasFirstMove = !piece.HasMoved;
piece.HasMoved = true;
HandleCastlingIfNeeded(gs, piece, oldPos, targetPos, wasFirstMove);
HandlePawnTwoSquareMove(gs, piece, oldPos, targetPos, wasFirstMove);
HandlePawnPromotionIfNeeded(piece, moveDto);
UpdateCastlingRights(gs, piece, oldPos);
gs.CurrentPlayer = gs.CurrentPlayer == PieceColor.White
? PieceColor.Black
: PieceColor.White;
}
private static void HandleEnPassantIfNeeded(GameState gs, ChessPiece piece, Position targetPos, ref ChessPiece? captured)
{
if (piece.Type != PieceType.Pawn || !gs.EnPassantTarget.HasValue)
return;
var enPassantSquare = gs.EnPassantTarget.Value;
if (targetPos.Row == enPassantSquare.Row && targetPos.Col == enPassantSquare.Col)
{
int direction = piece.Color == PieceColor.White ? 1 : -1;
var capturedPos = new Position(targetPos.Row + direction, targetPos.Col);
var potentialPawn = gs.Board[capturedPos.Row, capturedPos.Col];
if (potentialPawn != null && potentialPawn.Color != piece.Color && potentialPawn.Type == PieceType.Pawn)
{
gs.Board[capturedPos.Row, capturedPos.Col] = null;
potentialPawn.Position = new Position(-1, -1);
captured = potentialPawn;
}
}
}
private static void HandleCastlingIfNeeded(GameState gs, ChessPiece piece, Position oldPos, Position targetPos, bool wasFirstMove)
{
if (piece.Type != PieceType.King || !wasFirstMove)
return;
var colDiff = targetPos.Col - oldPos.Col;
if (Math.Abs(colDiff) == 2)
{
bool isKingside = colDiff > 0;
int rookStartCol = isKingside ? 7 : 0;
int rookEndCol = isKingside ? 5 : 3;
var rook = gs.Board[oldPos.Row, rookStartCol];
if (rook != null && rook.Type == PieceType.Rook && !rook.HasMoved)
{
gs.Board[oldPos.Row, rookStartCol] = null;
rook.Position = new Position(oldPos.Row, rookEndCol);
gs.Board[oldPos.Row, rookEndCol] = rook;
rook.HasMoved = true;
}
}
}
private static void HandlePawnTwoSquareMove(GameState gs, ChessPiece piece, Position oldPos, Position targetPos, bool wasFirstMove)
{
gs.EnPassantTarget = null;
if (piece.Type != PieceType.Pawn || !wasFirstMove)
return;
var rowDiff = Math.Abs(targetPos.Row - oldPos.Row);
if (rowDiff == 2)
{
var rowBehind = (oldPos.Row + targetPos.Row) / 2;
gs.EnPassantTarget = new Position(rowBehind, oldPos.Col);
}
}
private static void HandlePawnPromotionIfNeeded(ChessPiece piece, MoveDto moveDto)
{
if (piece.Type != PieceType.Pawn)
return;
bool promotionRow = piece.Color == PieceColor.White
? piece.Position.Row == 0
: piece.Position.Row == 7;
if (!promotionRow) return;
var promotionChoice = moveDto.PromotionChoice ?? PieceType.Queen;
piece.Type = promotionChoice;
}
private static void UpdateCastlingRights(GameState gs, ChessPiece movedPiece, Position oldPos)
{
if (movedPiece.Type == PieceType.King)
{
if (movedPiece.Color == PieceColor.White)
{
gs.WhiteCanCastleKingside = false;
gs.WhiteCanCastleQueenside = false;
}
else
{
gs.BlackCanCastleKingside = false;
gs.BlackCanCastleQueenside = false;
}
return;
}
if (movedPiece.Type == PieceType.Rook)
{
if (movedPiece.Color == PieceColor.White)
{
if (oldPos.Row == 7 && oldPos.Col == 0)
gs.WhiteCanCastleQueenside = false;
if (oldPos.Row == 7 && oldPos.Col == 7)
gs.WhiteCanCastleKingside = false;
}
else
{
if (oldPos.Row == 0 && oldPos.Col == 0)
gs.BlackCanCastleQueenside = false;
if (oldPos.Row == 0 && oldPos.Col == 7)
gs.BlackCanCastleKingside = false;
}
}
}
private IEnumerable<Position> GenerateCandidateMoves(GameState gs, ChessPiece piece)
{
return piece.Type switch
{
PieceType.Pawn => GeneratePawnMoves(gs, piece),
PieceType.Rook => GenerateRookMoves(gs, piece),
PieceType.Knight => GenerateKnightMoves(gs, piece),
PieceType.Bishop => GenerateBishopMoves(gs, piece),
PieceType.Queen => GenerateQueenMoves(gs, piece),
PieceType.King => GenerateKingMoves(gs, piece),
_ => []
};
}
private bool IsMoveLegalConsideringCheck(GameState gs, ChessPiece piece, Position target)
{
var clone = CloneGameState(gs);
var clonedPiece = clone.Pieces.First(p => p.Id == piece.Id);
var oldPos = clonedPiece.Position;
clone.Board[oldPos.Row, oldPos.Col] = null;
var captured = clone.Board[target.Row, target.Col];
if (captured != null) captured.Position = new Position(-1, -1);
clonedPiece.Position = target;
clone.Board[target.Row, target.Col] = clonedPiece;
HandleEnPassantOnClone(clone, clonedPiece, target);
HandleCastlingOnClone(clone, clonedPiece, oldPos, target);
var myKing = clone
.Pieces
.FirstOrDefault(p => p.Color == piece.Color
&& p.Type == PieceType.King
&& p.Position.Row >= 0);
if (myKing == null) return false;
bool inCheck = IsSquareAttacked(clone, myKing.Position, myKing.Color);
return !inCheck;
}
private static void HandleEnPassantOnClone(GameState clone, ChessPiece clonedPiece, Position target)
{
if (clonedPiece.Type != PieceType.Pawn || !clone.EnPassantTarget.HasValue)
return;
if (target.Row == clone.EnPassantTarget.Value.Row && target.Col == clone.EnPassantTarget.Value.Col)
{
int direction = clonedPiece.Color == PieceColor.White ? 1 : -1;
var capturedPos = new Position(target.Row + direction, target.Col);
var epCaptured = clone.Board[capturedPos.Row, capturedPos.Col];
if (epCaptured != null && epCaptured.Color != clonedPiece.Color && epCaptured.Type == PieceType.Pawn)
{
clone.Board[capturedPos.Row, capturedPos.Col] = null;
epCaptured.Position = new Position(-1, -1);
}
}
}
private static void HandleCastlingOnClone(GameState clone, ChessPiece clonedPiece, Position oldPos, Position target)
{
if (clonedPiece.Type != PieceType.King || clonedPiece.HasMoved)
return;
int colDiff = target.Col - oldPos.Col;
if (Math.Abs(colDiff) == 2)
{
bool isKingside = colDiff > 0;
int rookStartCol = isKingside ? 7 : 0;
int rookEndCol = isKingside ? 5 : 3;
var rook = clone.Board[oldPos.Row, rookStartCol];
if (rook != null && rook.Type == PieceType.Rook && !rook.HasMoved)
{
clone.Board[oldPos.Row, rookStartCol] = null;
rook.Position = new Position(oldPos.Row, rookEndCol);
clone.Board[oldPos.Row, rookEndCol] = rook;
}
}
}
private bool IsSquareAttacked(GameState gs, Position square, PieceColor colorOfSquare)
{
var enemyColor = colorOfSquare == PieceColor.White ? PieceColor.Black : PieceColor.White;
var enemyPieces = gs
.Pieces
.Where(p => p.Color == enemyColor && p.Position.Row >= 0)
.ToList();
foreach (var enemy in enemyPieces)
{
var moves = GenerateCandidateMoves(gs, enemy);
if (moves.Any(m => m.Row == square.Row && m.Col == square.Col))
return true;
}
return false;
}
private void UpdateCheckStatus(GameState gs)
{
gs.IsCheck = false;
gs.IsCheckmate = false;
gs.IsStalemate = false;
var king = gs
.Pieces
.FirstOrDefault(p => p.Color == gs.CurrentPlayer
&& p.Type == PieceType.King
&& p.Position.Row >= 0);
// If there's no king, that's effectively checkmate for that side.
if (king == null)
{
gs.IsCheck = true;
gs.IsCheckmate = true;
return;
}
bool inCheck = IsSquareAttacked(gs, king.Position, gs.CurrentPlayer);
gs.IsCheck = inCheck;
var allMoves = GetAllLegalMoves(gs);
if (allMoves.Count == 0 && inCheck)
{
gs.IsCheckmate = true;
return;
}
if (allMoves.Count == 0 && !inCheck)
{
gs.IsStalemate = true;
return;
}
}
private static GameState CloneGameState(GameState original)
{
var clone = new GameState
{
GameId = original.GameId,
CurrentPlayer = original.CurrentPlayer,
EnPassantTarget = original.EnPassantTarget,
WhiteCanCastleKingside = original.WhiteCanCastleKingside,
WhiteCanCastleQueenside = original.WhiteCanCastleQueenside,
BlackCanCastleKingside = original.BlackCanCastleKingside,
BlackCanCastleQueenside = original.BlackCanCastleQueenside,
MoveHistory = new List<string>(original.MoveHistory),
Board = new ChessPiece[8,8],
};
foreach (var p in original.Pieces)
{
var copy = new ChessPiece(p.Id, p.Type, p.Color, p.Position)
{
HasMoved = p.HasMoved
};
clone.Pieces.Add(copy);
}
foreach (var cp in clone.Pieces)
if (cp.Position.Row >= 0)
clone.Board[cp.Position.Row, cp.Position.Col] = cp;
return clone;
}
private static List<Position> GeneratePawnMoves(GameState gs, ChessPiece piece)
{
var moves = new List<Position>();
int direction = piece.Color == PieceColor.White ? -1 : 1;
var startRow = piece.Position.Row;
var startCol = piece.Position.Col;
var forward1 = startRow + direction;
if (IsOnBoard(forward1, startCol) && gs.Board[forward1, startCol] == null)
{
moves.Add(new Position(forward1, startCol));
if (!piece.HasMoved)
{
var forward2 = startRow + 2 * direction;
if (IsOnBoard(forward2, startCol) && gs.Board[forward2, startCol] == null)
moves.Add(new Position(forward2, startCol));
}
}
var leftCol = startCol - 1;
var rightCol = startCol + 1;
if (IsOnBoard(forward1, leftCol))
{
var occupant = gs.Board[forward1, leftCol];
if (occupant != null && occupant.Color != piece.Color)
moves.Add(new Position(forward1, leftCol));
}
if (IsOnBoard(forward1, rightCol))
{
var occupant = gs.Board[forward1, rightCol];
if (occupant != null && occupant.Color != piece.Color)
moves.Add(new Position(forward1, rightCol));
}
if (gs.EnPassantTarget.HasValue)
{
var ep = gs.EnPassantTarget.Value;
if (ep.Row == forward1 && Math.Abs(ep.Col - startCol) == 1)
moves.Add(ep);
}
return moves;
}
private static List<Position> GenerateRookMoves(GameState gs, ChessPiece piece)
=> GenerateSlidingMoves(gs, piece, [(1, 0), (-1, 0), (0, 1), (0, -1)]);
private static List<Position> GenerateBishopMoves(GameState gs, ChessPiece piece)
=> GenerateSlidingMoves(gs, piece, [(1, 1), (1, -1), (-1, 1), (-1, -1)]);
private static List<Position> GenerateQueenMoves(GameState gs, ChessPiece piece)
=> GenerateSlidingMoves(gs, piece,
[
(1, 0), (-1, 0), (0, 1), (0, -1),
(1, 1), (1, -1), (-1, 1), (-1, -1)
]);
private static List<Position> GenerateSlidingMoves(GameState gs, ChessPiece piece, (int dr, int dc)[] directions)
{
var results = new List<Position>();
var (startRow, startCol) = piece.Position;
foreach (var (dr, dc) in directions)
results.AddRange(GetSlidingMovesInDirection(gs, piece, startRow, startCol, dr, dc));
return results;
}
private static List<Position> GetSlidingMovesInDirection(
GameState gs,
ChessPiece piece,
int row,
int col,
int dr,
int dc)
{
var moves = new List<Position>();
var r = row;
var c = col;
while (true)
{
r += dr;
c += dc;
if (!IsOnBoard(r, c))
return moves;
var occupant = gs.Board[r, c];
if (occupant == null)
{
moves.Add(new Position(r, c));
continue;
}
if (occupant.Color != piece.Color)
moves.Add(new Position(r, c));
return moves;
}
}
private static IEnumerable<Position> GenerateKnightMoves(GameState gs, ChessPiece piece)
{
var offsets = new (int, int)[]
{
(2,1), (2,-1), (-2,1), (-2,-1),
(1,2), (1,-2), (-1,2), (-1,-2)
};
foreach (var (dr, dc) in offsets)
{
var r = piece.Position.Row + dr;
var c = piece.Position.Col + dc;
if (!IsOnBoard(r, c))
continue;
var occupant = gs.Board[r, c];
if (occupant == null || occupant.Color != piece.Color)
yield return new Position(r, c);
}
}
private List<Position> GenerateKingMoves(GameState gs, ChessPiece piece)
{
var results = new List<Position>();
var offsets = new[]
{
(1,0), (-1,0), (0,1), (0,-1),
(1,1), (1,-1), (-1,1), (-1,-1)
};
foreach (var (dr, dc) in offsets)
{
int r = piece.Position.Row + dr;
int c = piece.Position.Col + dc;
if (!IsOnBoard(r, c))
continue;
var occupant = gs.Board[r, c];
if (occupant == null || occupant.Color != piece.Color)
results.Add(new Position(r, c));
}
if (!piece.HasMoved && !gs.IsCheck && piece.Color == gs.CurrentPlayer)
AddCastlingMoves(gs, piece, results);
return results;
}
private void AddCastlingMoves(GameState gs, ChessPiece king, List<Position> results)
{
bool hasKingsideRight = king.Color == PieceColor.White
? gs.WhiteCanCastleKingside
: gs.BlackCanCastleKingside;
bool hasQueensideRight = king.Color == PieceColor.White
? gs.WhiteCanCastleQueenside
: gs.BlackCanCastleQueenside;
var row = king.Position.Row;
var col = king.Position.Col;
bool areKingsideCastleSpacesEmpty = IsEmpty(row, col + 1, gs)
&& IsEmpty(row, col + 2, gs);
bool isKingsideCastleSafe = !IsSquareAttacked(gs, new Position(row, col + 1), king.Color)
&& !IsSquareAttacked(gs, new Position(row, col + 2), king.Color);
bool areQueensideCastleSpacesEmpty = IsEmpty(row, col - 1, gs)
&& IsEmpty(row, col - 2, gs)
&& IsEmpty(row, col - 3, gs);
bool isQueensideCastleSafe = !IsSquareAttacked(gs, new Position(row, col - 1), king.Color)
&& !IsSquareAttacked(gs, new Position(row, col - 2), king.Color);
bool canCastleKingside = areKingsideCastleSpacesEmpty
&& isKingsideCastleSafe
&& hasKingsideRight;
bool canCastleQueenside = areQueensideCastleSpacesEmpty
&& isQueensideCastleSafe
&& hasQueensideRight;
if (canCastleKingside) results.Add(new Position(row, col + 2));
if (canCastleQueenside) results.Add(new Position(row, col - 2));
}
private static bool IsEmpty(int r, int c, GameState gs)
{
if (!IsOnBoard(r, c)) return false;
return gs.Board[r, c] == null;
}
private static bool IsOnBoard(int r, int c)
=> r >= 0 && r < 8 && c >= 0 && c < 8;
}
@@ -0,0 +1,11 @@
using JoshHeaps.Net.Models;
namespace JoshHeaps.Net.Services.Interfaces;
public interface IChessService
{
GameState CreateNewGame();
List<(ChessPiece piece, List<Position> moves)> GetAllLegalMoves(GameState gameState);
List<Position> GetLegalMovesForPiece(GameState gameState, string pieceId);
MoveResultDto MakeMove(GameState gameState, MoveDto moveDto);
}
+40
View File
@@ -0,0 +1,40 @@
#chessBoard {
margin: 2vw auto;
width: 60vw;
height: 60vw;
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
border: 0px;
}
.chessSquare {
background-color: inherit;
position: relative;
}
.chessSquare.light {
background-color: #ccc;
}
.chessSquare.dark {
background-color: #656770;
}
.chessPiece {
width: 90%;
height: 90%;
display: block;
margin: auto;
}
.chessSquare.selected {
border: 4px solid red;
box-sizing: border-box;
}
.chessSquare.legal {
border: 4px dashed red;
box-sizing: border-box;
cursor: pointer;
}
+106
View File
@@ -0,0 +1,106 @@
html, body {
background-color: #2b2c30;
color: #d6d6d6;
cursor: default;
height: 100%;
margin: 0;
}
#startGameBtn {
background-color: #8cd5ed;
color: #262626;
border-radius: 30px;
border: 0px;
cursor: pointer;
order: 1;
}
#chessContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
height: 100vh;
box-sizing: border-box;
padding: 5vw;
}
#chessBoard {
aspect-ratio: 1 / 1;
width: 90vw; /* use the smaller of width or height */
height: auto;
max-height: 90vw;
}
#boardContainer {
align-content: center;
flex-grow: 1;
}
.sideContent {
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
text-align: center;
}
#textContainer {
flex-direction: column;
order: -1;
flex-shrink: 1;
}
#buttonContainer {
flex-grow: 3;
}
/* Put UI to left/right when screen is short */
@media (min-aspect-ratio: 1/1) {
#chessContainer {
display: grid;
grid-template-columns: auto, auto;
grid-template-rows: auto, auto;
align-items: center;
flex-direction: row;
justify-content: center;
align-items: center;
height: 100vh;
padding: 0;
}
.sideContent {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 0;
align-items: center;
text-align: center;
padding: min(5vw, 5vh);
}
#boardContainer {
grid-row: 1 / span 2;
grid-column: 1;
}
#textContainer {
grid-row: 1;
grid-column: 2;
text-align: center;
}
#buttonContainer {
grid-row: 2;
grid-column: 2;
text-align: left;
align-self: start;
}
#chessBoard {
aspect-ratio: 1 / 1;
flex-shrink: 1;
width: min(90vh, 90vw);
max-height: 90vh;
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.6)">
<g style="fill:#000000; stroke:#000000; stroke-linecap:butt;">
<path d="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
<path d="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
<path d="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
</g>
<path d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" style="fill:none; stroke:#ffffff; stroke-linejoin:miter;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;">
<path d="M 22.5,11.63 L 22.5,6" style="fill:none; stroke:#000000; stroke-linejoin:miter;" id="path6570"/>
<path d="M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25" style="fill:#000000;fill-opacity:1; stroke-linecap:butt; stroke-linejoin:miter;"/>
<path d="M 12.5,37 C 18,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 20,16 10.5,13 6.5,19.5 C 3.5,25.5 12.5,30 12.5,30 L 12.5,37" style="fill:#000000; stroke:#000000;"/>
<path d="M 20,8 L 25,8" style="fill:none; stroke:#000000; stroke-linejoin:miter;"/>
<path d="M 32,29.5 C 32,29.5 40.5,25.5 38.03,19.85 C 34.15,14 25,18 22.5,24.5 L 22.5,26.6 L 22.5,24.5 C 20,18 10.85,14 6.97,19.85 C 4.5,25.5 13,29.5 13,29.5" style="fill:none; stroke:#ffffff;"/>
<path d="M 12.5,30 C 18,27 27,27 32.5,30 M 12.5,33.5 C 18,30.5 27,30.5 32.5,33.5 M 12.5,37 C 18,34 27,34 32.5,37" style="fill:none; stroke:#ffffff;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
<path
d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"
style="fill:#000000; stroke:#000000;" />
<path
d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"
style="fill:#000000; stroke:#000000;" />
<path
d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"
style="fill:#ffffff; stroke:#ffffff;" />
<path
d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z"
transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)"
style="fill:#ffffff; stroke:#ffffff;" />
<path
d="M 24.55,10.4 L 24.1,11.85 L 24.6,12 C 27.75,13 30.25,14.49 32.5,18.75 C 34.75,23.01 35.75,29.06 35.25,39 L 35.2,39.5 L 37.45,39.5 L 37.5,39 C 38,28.94 36.62,22.15 34.25,17.66 C 31.88,13.17 28.46,11.02 25.06,10.5 L 24.55,10.4 z "
style="fill:#ffffff; stroke:none;" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<path d="m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z" style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 766 B

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45"
height="45">
<g style="fill:#000000;stroke:#000000;stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round">
<path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"
style="stroke-linecap:butt;fill:#000000" />
<path d="m 9,26 c 0,2 1.5,2 2.5,4 1,1.5 1,1 0.5,3.5 -1.5,1 -1,2.5 -1,2.5 -1.5,1.5 0,2.5 0,2.5 6.5,1 16.5,1 23,0 0,0 1.5,-1 0,-2.5 0,0 0.5,-1.5 -1,-2.5 -0.5,-2.5 -0.5,-2 0.5,-3.5 1,-2 2.5,-2 2.5,-4 -8.5,-1.5 -18.5,-1.5 -27,0 z" />
<path d="M 11.5,30 C 15,29 30,29 33.5,30" />
<path d="m 12,33.5 c 6,-1 15,-1 21,0" />
<circle cx="6" cy="12" r="2" />
<circle cx="14" cy="9" r="2" />
<circle cx="22.5" cy="8" r="2" />
<circle cx="31" cy="9" r="2" />
<circle cx="39" cy="12" r="2" />
<path d="M 11,38.5 A 35,35 1 0 0 34,38.5"
style="fill:none; stroke:#000000;stroke-linecap:butt;" />
<g style="fill:none; stroke:#ffffff;">
<path d="M 11,29 A 35,35 1 0 1 34,29" />
<path d="M 12.5,31.5 L 32.5,31.5" />
<path d="M 11.5,34.5 A 35,35 1 0 0 33.5,34.5" />
<path d="M 10.5,37.5 A 35,35 1 0 0 34.5,37.5" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
<path
d="M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z "
style="stroke-linecap:butt;" />
<path
d="M 12.5,32 L 14,29.5 L 31,29.5 L 32.5,32 L 12.5,32 z "
style="stroke-linecap:butt;" />
<path
d="M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z "
style="stroke-linecap:butt;" />
<path
d="M 14,29.5 L 14,16.5 L 31,16.5 L 31,29.5 L 14,29.5 z "
style="stroke-linecap:butt;stroke-linejoin:miter;" />
<path
d="M 14,16.5 L 11,14 L 34,14 L 31,16.5 L 14,16.5 z "
style="stroke-linecap:butt;" />
<path
d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14 L 11,14 z "
style="stroke-linecap:butt;" />
<path
d="M 12,35.5 L 33,35.5 L 33,35.5"
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
<path
d="M 13,31.5 L 32,31.5"
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
<path
d="M 14,29.5 L 31,29.5"
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
<path
d="M 14,16.5 L 31,16.5"
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
<path
d="M 11,14 L 34,14"
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.6)">
<g style="fill:#ffffff; stroke:#000000; stroke-linecap:butt;">
<path d="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
<path d="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
<path d="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
</g>
<path d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" style="fill:none; stroke:#000000; stroke-linejoin:miter;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45">
<g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path stroke-linejoin="miter" d="M22.5 11.63V6M20 8h5"/>
<path fill="#fff" stroke-linecap="butt" stroke-linejoin="miter" d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5"/>
<path fill="#fff" d="M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7"/>
<path d="M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 700 B

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
<path
d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"
style="fill:#ffffff; stroke:#000000;" />
<path
d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"
style="fill:#ffffff; stroke:#000000;" />
<path
d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"
style="fill:#000000; stroke:#000000;" />
<path
d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z"
transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)"
style="fill:#000000; stroke:#000000;" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<path d="m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z" style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 766 B

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="fill:#ffffff;stroke:#000000;stroke-width:1.5;stroke-linejoin:round">
<path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"/>
<path d="M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 11,36 11,36 C 9.5,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z"/>
<path d="M 11.5,30 C 15,29 30,29 33.5,30" style="fill:none"/>
<path d="M 12,33.5 C 18,32.5 27,32.5 33,33.5" style="fill:none"/>
<circle cx="6" cy="12" r="2" />
<circle cx="14" cy="9" r="2" />
<circle cx="22.5" cy="8" r="2" />
<circle cx="31" cy="9" r="2" />
<circle cx="39" cy="12" r="2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
<path
d="M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z "
style="stroke-linecap:butt;" />
<path
d="M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z "
style="stroke-linecap:butt;" />
<path
d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14"
style="stroke-linecap:butt;" />
<path
d="M 34,14 L 31,17 L 14,17 L 11,14" />
<path
d="M 31,17 L 31,29.5 L 14,29.5 L 14,17"
style="stroke-linecap:butt; stroke-linejoin:miter;" />
<path
d="M 31,29.5 L 32.5,32 L 12.5,32 L 14,29.5" />
<path
d="M 11,14 L 34,14"
style="fill:none; stroke:#000000; stroke-linejoin:miter;" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,219 @@
let currentGameId = null;
let currentPlayerId = null;
let signalRConnection = null;
let selectedPiece = null;
let legalMoves = [];
async function startNewGame() {
// Stop previous SignalR connection if needed
if (signalRConnection) {
await signalRConnection.stop();
signalRConnection = null;
}
// Join the game via API
const response = await fetch('/api/chess/JoinGame');
const gameData = await response.json();
currentGameId = gameData.gameId;
currentPlayerId = gameData.id;
console.log("🆕 Game started:", currentGameId);
// Build and start SignalR connection
signalRConnection = new signalR.HubConnectionBuilder()
.withUrl("/chessHub")
.configureLogging(signalR.LogLevel.Information)
.build();
signalRConnection.onclose(err => {
console.error("❌ SignalR connection closed:", err?.message);
});
signalRConnection.on("ReceiveMoveUpdate", async (gameId, moveResultDto) => {
if (gameId !== currentGameId) return;
const res = await fetch(`/api/chess/${gameId}`);
const data = await res.json();
renderPieces(data.pieces);
alertGameStatusChange(moveResultDto);
});
try {
await signalRConnection.start();
console.log("✅ SignalR connected");
await signalRConnection.invoke("JoinWebsocketGroup", currentGameId);
} catch (err) {
console.error("❌ SignalR failed to start or join:", err);
}
// Render initial state
const gameState = await fetch(`/api/chess/${currentGameId}`);
const data = await gameState.json();
renderPieces(data.pieces);
}
function renderPieces(pieces) {
// Clear all squares
for (let i = 0; i < 64; i++) {
const square = document.getElementById(`square-${i}`);
square.innerHTML = ""; // Remove any previous images
}
// Place each piece
pieces.forEach(piece => {
const index = piece.row * 8 + piece.col;
const square = document.getElementById(`square-${index}`);
if (!square) return;
const img = document.createElement("img");
img.src = getPieceImageUrl(piece);
img.alt = piece.type;
img.classList.add("chessPiece");
img.onclick = (e) => {
e.stopPropagation(); // 👈 Prevents the parent square click from firing
handlePieceClick(piece);
console.log("Clicked on:", piece);
};
square.appendChild(img);
});
for (let i = 0; i < 64; i++) {
const square = document.getElementById(`square-${i}`);
if (!square.classList.contains("legal")) {
square.onclick = () => clearHighlights();
}
}
}
function getPieceImageUrl(piece) {
const basePath = "/images/Chess Images/";
const color = piece.color === 0 ? "White" : "Black"; // Or use "White"/"Black" if string
const typeMap = {
0: "Pawn",
1: "Rook",
2: "Knight",
3: "Bishop",
4: "Queen",
5: "King"
};
return basePath + color + typeMap[piece.type] + ".svg";
}
async function handlePieceClick(piece) {
clearHighlights();
selectedPiece = piece;
try {
const res = await fetch(`/api/chess/${currentGameId}/legalMoves/${piece.id}`);
if (!res.ok) throw new Error("API failed");
legalMoves = await res.json();
highlightSelected(piece.row, piece.col);
highlightLegalMoves(legalMoves);
} catch (err) {
console.error("Error in handlePieceClick:", err);
}
}
function highlightSelected(row, col) {
document.getElementById(`square-${row * 8 + col}`).classList.add("selected");
}
function highlightLegalMoves(moves) {
moves.forEach(move => {
const index = move.row * 8 + move.col;
const square = document.getElementById(`square-${index}`);
square.classList.add("legal");
// Remove click from piece (if present) so square click handles it
const img = square.querySelector("img");
if (img) img.onclick = null;
// Let the square itself handle the move
square.onclick = () => handleMove(move.row, move.col);
});
}
async function handleMove(targetRow, targetCol) {
if (!selectedPiece) return;
const moveDto = {
GameId: currentGameId,
PlayerId: currentPlayerId,
PieceId: selectedPiece.id,
SourceRow: selectedPiece.row,
SourceCol: selectedPiece.col,
TargetRow: targetRow,
TargetCol: targetCol,
PromotionChoice: null // optional
};
const res = await fetch("/api/chess/move", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(moveDto)
});
const moveResultDto = await res.json();
if (!res.ok || !moveResultDto.success) {
alert("❌ " + (moveResultDto.message || "Invalid move or not your turn."));
return;
}
const updatedGame = await fetch(`/api/chess/${currentGameId}`).then(r => r.json());
renderPieces(updatedGame.pieces);
// Reset state
selectedPiece = null;
legalMoves = [];
clearHighlights();
await signalRConnection.invoke("MoveMade", currentGameId, moveResultDto);
alertGameStatusChange(moveResultDto);
}
function clearHighlights() {
for (let i = 0; i < 64; i++) {
const square = document.getElementById(`square-${i}`);
// Remove legal move markers and their onclicks
if (square.classList.contains("legal")) {
square.classList.remove("legal");
square.onclick = null;
}
// Always remove selection styling
square.classList.remove("selected");
}
selectedPiece = null;
legalMoves = [];
}
function alertGameStatusChange(moveResultDto) {
setTimeout(() => {
// 🟡 Optional: show check
if (moveResultDto.isCheck) {
console.log("🛑 Check!");
}
// ✅ Handle game end
if (moveResultDto.isCheckmate) {
alert("♟️ Checkmate!");
gameOver = true;
} else if (moveResultDto.isStalemate) {
alert("🤝 Stalemate!");
gameOver = true;
}
}, 500);
}
console.log("chessLogic.js loaded");
window.startNewGame = startNewGame;
File diff suppressed because one or more lines are too long
+102
View File
@@ -0,0 +1,102 @@
using System.Text.Json;
using System.Text;
class ChessApiTest
{
private static readonly HttpClient client = new HttpClient { BaseAddress = new Uri("https://localhost:7118/api/") };
static async Task Main()
{
Console.WriteLine("Starting Chess API Test...");
var gameId = await CreateGame();
var (player1Id, isWhite1) = await JoinGame();
var (player2Id, isWhite2) = await JoinGame();
await GetGameState(gameId);
var pawnId = "wPawn4";
await GetLegalMoves(gameId, pawnId);
await MakeMove(gameId, player1Id, pawnId, 6, 4, 4, 4); // e2 to e4
await MakeMove(gameId, player2Id, "bPawn4", 1, 4, 2, 4); // e7 to e6
await MakeMove(gameId, player1Id, "wKing", 7, 4, 7, 6); // Try castling
await MakeMove(gameId, player1Id, "wPawn4", 4, 4, 3, 4); // Move bishop
await MakeMove(gameId, player2Id, "bPawn3", 1, 3, 3, 3); // d7 to d5
await MakeMove(gameId, player1Id, "wPawn4", 4, 4, 2, 3); // En passant capture
await MakeMove(gameId, player1Id, "wPawn7", 6, 7, 7, 7, PieceType.Queen); // Pawn Promotion
await GetGameState(gameId);
Console.WriteLine("Test completed.");
}
private static async Task<Guid> CreateGame()
{
var response = await client.PostAsync("Chess/new", null);
var data = JsonSerializer.Deserialize<Dictionary<string, object>>(await response.Content.ReadAsStringAsync());
Guid gameId = Guid.Parse(data["gameId"].ToString());
Console.WriteLine($"Game created: {gameId}");
return gameId;
}
private static async Task<(Guid, bool)> JoinGame()
{
var response = await client.GetAsync("Chess/JoinGame");
var data = JsonSerializer.Deserialize<Dictionary<string, object>>(await response.Content.ReadAsStringAsync());
Guid playerId = Guid.Parse(data["id"].ToString());
bool isWhite = bool.Parse(data["isWhite"].ToString());
Console.WriteLine($"Player joined: {playerId} (IsWhite: {isWhite})");
return (playerId, isWhite);
}
private static async Task GetGameState(Guid gameId)
{
var response = await client.GetAsync($"Chess/{gameId}");
Console.WriteLine($"Game state: {await response.Content.ReadAsStringAsync()}");
}
private static async Task GetLegalMoves(Guid gameId, string pieceId)
{
var response = await client.GetAsync($"Chess/{gameId}/legalMoves/{pieceId}");
Console.WriteLine($"Legal moves for {pieceId}: {await response.Content.ReadAsStringAsync()}");
}
private static async Task MakeMove(Guid gameId, Guid playerId, string pieceId, int sourceRow, int sourceCol, int targetRow, int targetCol, PieceType? promotion = null)
{
var payload = new
{
GameId = gameId,
PlayerId = playerId,
PieceId = pieceId,
SourceRow = sourceRow,
SourceCol = sourceCol,
TargetRow = targetRow,
TargetCol = targetCol,
PromotionChoice = promotion
};
var jsonPayload = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await client.PostAsync("Chess/move", jsonPayload);
Console.WriteLine($"Move {pieceId} to ({targetRow}, {targetCol}): {await response.Content.ReadAsStringAsync()}");
}
}
public enum PieceType
{
Pawn,
Rook,
Knight,
Bishop,
Queen,
King
}
+10
View File
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>