Add chess (Oh my gosh big commit)
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.9.34728.123
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net", "JoshHeaps.Net\JoshHeaps.Net.csproj", "{9F0182CC-470F-4D1A-99F5-348D7921751E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{82206AD3-19DF-4DDA-9647-02B691B78CE8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,12 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\images\Chess Images\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace JoshHeaps.Net.Models;
|
||||
|
||||
public enum PieceType
|
||||
{
|
||||
Pawn,
|
||||
Rook,
|
||||
Knight,
|
||||
Bishop,
|
||||
Queen,
|
||||
King
|
||||
}
|
||||
|
||||
public enum PieceColor
|
||||
{
|
||||
White,
|
||||
Black
|
||||
}
|
||||
@@ -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. Let’s 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 = [];
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"]" />
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JoshHeaps.Net.Pages
|
||||
{
|
||||
public class ChessModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,98 @@
|
||||
@page
|
||||
@model JoshHeaps.Net.Pages.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "Home page";
|
||||
ViewData["Title"] = "JoshHeaps.Net";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<html lang="en-us">
|
||||
<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('contact')">./ Contact me</a>
|
||||
</div>
|
||||
<div id="Header">
|
||||
<a class="headerOption" onclick="showClickedContents('projects')">./ Projects</a>
|
||||
<a class="headerOption" onclick="showClickedContents('contact')">./ Contact me</a>
|
||||
</div>
|
||||
|
||||
<div id="WelcomeMessage"></div>
|
||||
<div id="WelcomeMessage"></div>
|
||||
|
||||
<div id="ProjectsBox">
|
||||
<div id="ChessProject" class="projectContainer">
|
||||
<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 id="CompilerProject" class="projectContainer">
|
||||
<div id="CompilerText" class="projectTextLeft">
|
||||
<h2 id="CompilerTitle" class="projectHeaderLeft projectHeader">Compiler</h2>
|
||||
<p id="CompilerDescription" class="projectDescriptionLeft projectDescription">
|
||||
I love solving problems, and figuring out how things work. What better way to do both than writing a compiler myself? This project is a compiler that I wrote in C# on .NET 8.0. It takes in a simple language that I created, outputs the corresponding common intermediate language (CIL), and executes it. I plan to add more features in the future.
|
||||
</p>
|
||||
</div>
|
||||
<div class="displayBox diagonal-section-right">
|
||||
<a target="_blank" href="https://github.com/JoshHeaps/CILCompiler">
|
||||
<img id="CompilerImage" class="projectImage projectImageRight" src="/images/CompilerDemo.png" title="Compiler Input and Output"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="Contacts">
|
||||
<a id="GithubLink" class="social-link" href="https://github.com/JoshHeaps" target="_blank">
|
||||
<svg viewBox="0 0 16 16" class="social-icon">
|
||||
<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>
|
||||
</a>
|
||||
<a id="LinkedinLink" class="social-link" href="https://www.linkedin.com/in/josh-heaps/" target="_blank">
|
||||
<svg viewBox="0 0 24 24" class="social-icon">
|
||||
<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>
|
||||
</a>
|
||||
<a id="GmailLink" class="social-link" href="mailto:[email protected]">
|
||||
<svg viewBox="0 -4 31 31" class="social-icon" preserveAspectRatio="none">
|
||||
<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="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="m15 8.35227273-8.18181818-6.13636364v8.86363641l8.18181818 6.1363636 8.1818182-6.1363636v-8.86363641z"/>
|
||||
</svg>
|
||||
<div id="ProjectsBox">
|
||||
<div id="ChessProject" class="projectContainer">
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
<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 id="CompilerProject" class="projectContainer">
|
||||
<div id="CompilerText" class="projectTextLeft">
|
||||
<h2 id="CompilerTitle" class="projectHeaderLeft projectHeader">Compiler</h2>
|
||||
<p id="CompilerDescription" class="projectDescriptionLeft projectDescription">
|
||||
I love solving problems, and figuring out how things work. What better way to do both than writing a compiler myself? This project is a compiler that I wrote in C# on .NET 8.0. It takes in a simple language that I created, outputs the corresponding common intermediate language (CIL), and executes it. I plan to add more features in the future.
|
||||
</p>
|
||||
</div>
|
||||
<div class="displayBox diagonal-section-right">
|
||||
<a target="_blank" href="https://github.com/JoshHeaps/CILCompiler">
|
||||
<img id="CompilerImage" class="projectImage projectImageRight" src="/images/CompilerDemo.png" title="Compiler Input and Output"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ChessProject" class="projectContainer">
|
||||
<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">
|
||||
<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>
|
||||
<rect width="100%" height="100%" fill="transparent" />
|
||||
</a>
|
||||
</svg>
|
||||
|
||||
<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>
|
||||
<rect width="100%" height="100%" fill="transparent" />
|
||||
</a>
|
||||
</svg>
|
||||
|
||||
<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="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="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"/>
|
||||
<rect width="100%" height="100%" fill="transparent" />
|
||||
</a>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@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>
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JoshHeaps.Net.Pages
|
||||
{
|
||||
public class _LayoutModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.Services.Implementations;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
builder.Services.AddSingleton<IChessService, ChessService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
@@ -14,7 +24,16 @@ if (!app.Environment.IsDevelopment())
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -22,4 +41,8 @@ app.UseAuthorization();
|
||||
|
||||
app.MapRazorPages();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapHub<ChessHub>("/chessHub");
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||