Add stockfish to chess
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace JoshHeaps.Net.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ChessController : ControllerBase
|
||||
public class ChessController(IChessService chessService, IHubContext<ChessHub> chessHub, IBackgroundTaskQueue queue) : ControllerBase
|
||||
{
|
||||
private readonly IChessService chessService;
|
||||
|
||||
/// <summary>
|
||||
/// Store of ongoing games.
|
||||
/// </summary>
|
||||
@@ -18,21 +18,50 @@ public class ChessController : ControllerBase
|
||||
|
||||
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()
|
||||
[HttpGet("new")]
|
||||
[HttpGet("new/{difficulty}")]
|
||||
public ActionResult CreateGame(int difficulty = 20)
|
||||
{
|
||||
var gameState = chessService.CreateNewGame();
|
||||
_games[gameState.GameId] = gameState;
|
||||
|
||||
return Ok(new { gameState.GameId });
|
||||
gameState.IsVsComputer = true;
|
||||
gameState.WhiteJoined = true;
|
||||
gameState.BlackJoined = true;
|
||||
Guid playerId = Guid.NewGuid();
|
||||
Guid computerId = Guid.NewGuid();
|
||||
var isWhite = Random.Shared.Next(2) == 0;
|
||||
|
||||
gameState.Computer = new(difficulty);
|
||||
|
||||
if (isWhite)
|
||||
{
|
||||
gameState.WhitePlayerId = playerId;
|
||||
gameState.BlackPlayerId = computerId;
|
||||
}
|
||||
else
|
||||
{
|
||||
gameState.WhitePlayerId = computerId;
|
||||
gameState.BlackPlayerId = playerId;
|
||||
queue.Queue(async () =>
|
||||
{
|
||||
// Give user's browser time to connect to signalR and such.
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
await gameState.Computer.MakeMove(gameState, chessHub, chessService);
|
||||
});
|
||||
}
|
||||
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1));
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Id = playerId,
|
||||
IsWhite = isWhite,
|
||||
gameState.GameId
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -153,9 +182,15 @@ public class ChessController : ControllerBase
|
||||
else
|
||||
{
|
||||
// increase timeout if play continues.
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1));
|
||||
if (gameState.IsVsComputer)
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1));
|
||||
else
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1));
|
||||
}
|
||||
|
||||
if (gameState.IsVsComputer && gameState.Computer is not null)
|
||||
queue.Queue(() => gameState.Computer.MakeMove(gameState, chessHub, chessService));
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
@@ -199,6 +234,10 @@ public class ChessController : ControllerBase
|
||||
_gameRemovalTasks[id] = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(delay);
|
||||
|
||||
if (_games[id].Computer is not null)
|
||||
await _games[id].Computer!.DisposeAsync();
|
||||
|
||||
_games.Remove(id, out _);
|
||||
_gameRemovalTasks.Remove(id, out _);
|
||||
});
|
||||
@@ -209,6 +248,10 @@ public class ChessController : ControllerBase
|
||||
_gameRemovalTasks.TryAdd(id, Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(delay);
|
||||
|
||||
if (_games[id].Computer is not null)
|
||||
await _games[id].Computer!.DisposeAsync();
|
||||
|
||||
_games.Remove(id, out _);
|
||||
_gameRemovalTasks.Remove(id, out _);
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
namespace JoshHeaps.Net.Models;
|
||||
|
||||
public class GameState
|
||||
{
|
||||
@@ -39,6 +41,10 @@ public class GameState
|
||||
public Guid WhitePlayerId { get; set; }
|
||||
public Guid BlackPlayerId { get; set; }
|
||||
|
||||
public bool IsVsComputer { get; set; } = false;
|
||||
|
||||
public Stockfish? Computer { get; set; }
|
||||
|
||||
// optional: convenience
|
||||
public bool IsOpen => !WhiteJoined || !BlackJoined;
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
</div>
|
||||
|
||||
<div id="textContainer" class="sideContent">
|
||||
<h1>Chess Arena</h1>
|
||||
<p>This page will host your chess UI and interactions.</p>
|
||||
<h1>Chess</h1>
|
||||
<p>Click a button to start a game :)</p>
|
||||
</div>
|
||||
|
||||
<div id="buttonContainer" class="sideContent">
|
||||
<button id="startGameBtn" onclick="startNewGame()">Start New Game</button>
|
||||
<button id="startCPUGame" onclick="startCPUGame()">Start Game Against Computer</button>
|
||||
<button id="startCPUGame" onclick="startCPUGame()">Vs CPU</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="difficultyModal" style="display: none;">
|
||||
<p>Set bot difficulty to:</p>
|
||||
<div id="difficultyButtonContainer">
|
||||
@for (int i = 1; i <= 20; i++)
|
||||
{
|
||||
<button onclick="selectDifficulty(@i)">
|
||||
@i
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/signalr/signalr.min.js"></script>
|
||||
<script src="~/js/ChessScripts/chessLogic.js"></script>
|
||||
|
||||
@@ -26,6 +26,7 @@ public class Program
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
builder.Services.AddSingleton<IChessService, ChessService>();
|
||||
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
public class BackgroundTaskQueue : IBackgroundTaskQueue
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, Task> _runningTasks = new();
|
||||
|
||||
public void Queue(Func<Task> workItem)
|
||||
{
|
||||
var task = Task.Run(workItem);
|
||||
_runningTasks.TryAdd(task.Id, task);
|
||||
|
||||
task.ContinueWith(t => _runningTasks.TryRemove(t.Id, out _), TaskScheduler.Default);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<Task> Running => [.. _runningTasks.Values];
|
||||
public Task WhenAllDone() => Task.WhenAll(Running);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
@@ -11,13 +14,15 @@ public sealed class Stockfish : IAsyncDisposable
|
||||
private readonly Process _p;
|
||||
private readonly StreamWriter _stdin;
|
||||
private readonly Channel<string> _stdout = Channel.CreateUnbounded<string>();
|
||||
private readonly int _skill;
|
||||
|
||||
public Stockfish(int skill = 20, int hash = 256)
|
||||
{
|
||||
string relativeFilePath = @"JoshHeaps.Net\Resources\stockfish-windows-x86-64-avx2.exe";
|
||||
string exePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!
|
||||
.Split("JoshHeaps.Net")
|
||||
.First() + relativeFilePath);
|
||||
_skill = skill;
|
||||
string relativeFilePath = @"\Resources\stockfish-windows-x86-64-avx2.exe";
|
||||
string exePath = Assembly.GetExecutingAssembly().Location.Split(@"\bin\")[0] + relativeFilePath;
|
||||
|
||||
Console.Write(exePath);
|
||||
|
||||
_p = new Process
|
||||
{
|
||||
@@ -50,10 +55,10 @@ public sealed class Stockfish : IAsyncDisposable
|
||||
WaitFor("readyok").GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string> GetBestMoveAsync(string fen, int millis = 1000)
|
||||
public async Task<string> GetBestMoveAsync(string fen)
|
||||
{
|
||||
Send($"position fen {fen}");
|
||||
Send($"go movetime {millis}");
|
||||
Send($"go depth {_skill}");
|
||||
string? best = null;
|
||||
|
||||
await foreach (var line in _stdout.Reader.ReadAllAsync())
|
||||
@@ -82,32 +87,75 @@ public sealed class Stockfish : IAsyncDisposable
|
||||
await _p.WaitForExitAsync();
|
||||
_p.Dispose();
|
||||
}
|
||||
|
||||
public async Task MakeMove(GameState state, IHubContext<ChessHub> chessHub, IChessService chessService)
|
||||
{
|
||||
var move = await GetBestMoveAsync(state.ToFen());
|
||||
|
||||
var moveDto = move.ToMoveDto(
|
||||
state,
|
||||
state.CurrentPlayer == PieceColor.White
|
||||
? state.WhitePlayerId
|
||||
: state.BlackPlayerId);
|
||||
|
||||
var result = chessService.MakeMove(state, moveDto);
|
||||
|
||||
await chessHub.Clients.Group(state.GameId.ToString()).SendAsync("ReceiveMoveUpdate", state.GameId.ToString(), moveDto, result);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StockfishHelpers
|
||||
{
|
||||
public static MoveDto ToMoveDto(
|
||||
this string uci,
|
||||
GameState gameState,
|
||||
Guid playerId)
|
||||
{
|
||||
int fCol = uci[0] - 'a', fRow = 7 - (uci[1] - '1');
|
||||
int tCol = uci[2] - 'a', tRow = 7 - (uci[3] - '1');
|
||||
|
||||
var piece = gameState.Board[fRow, fCol]
|
||||
?? throw new Exception("No piece at source square");
|
||||
|
||||
PieceType? promo = uci.Length == 5 ? uci[4] switch
|
||||
{
|
||||
'q' => PieceType.Queen,
|
||||
'r' => PieceType.Rook,
|
||||
'b' => PieceType.Bishop,
|
||||
'n' => PieceType.Knight,
|
||||
_ => null
|
||||
} : null;
|
||||
|
||||
return new MoveDto
|
||||
{
|
||||
GameId = gameState.GameId,
|
||||
PlayerId = playerId,
|
||||
PieceId = piece.Id,
|
||||
TargetRow = tRow,
|
||||
TargetCol = tCol,
|
||||
PromotionChoice = promo,
|
||||
SourceCol = fCol,
|
||||
SourceRow = fRow,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a 2-D board array (rank 8 = row 0, file a = col 0) to a FEN string.
|
||||
/// Only piece placement + active colour + castling are computed; the rest use
|
||||
/// safe defaults (-, 0, 1). That is all Stockfish needs.
|
||||
/// </summary>
|
||||
public static string ToFen(
|
||||
ChessPiece?[,] board,
|
||||
PieceColor activeColour = PieceColor.White)
|
||||
public static string ToFen(this GameState gs)
|
||||
{
|
||||
if (board.GetLength(0) != 8 || board.GetLength(1) != 8)
|
||||
throw new ArgumentException("Board must be 8×8.");
|
||||
|
||||
var sb = new StringBuilder(64);
|
||||
|
||||
// ----- 1) piece placement -----
|
||||
for (int rank = 0; rank < 8; rank++)
|
||||
/* 1) piece placement */
|
||||
for (int row = 0; row < 8; row++)
|
||||
{
|
||||
int empty = 0;
|
||||
|
||||
for (int file = 0; file < 8; file++)
|
||||
for (int col = 0; col < 8; col++)
|
||||
{
|
||||
var p = board[rank, file];
|
||||
var p = gs.Board[row, col];
|
||||
|
||||
if (p is null)
|
||||
{
|
||||
@@ -116,28 +164,54 @@ public static class StockfishHelpers
|
||||
else
|
||||
{
|
||||
if (empty > 0) { sb.Append(empty); empty = 0; }
|
||||
sb.Append(ToFenChar(p));
|
||||
sb.Append(ToFenChar(p)); // ← unchanged helper
|
||||
}
|
||||
}
|
||||
|
||||
if (empty > 0) sb.Append(empty);
|
||||
if (rank < 7) sb.Append('/');
|
||||
if (row < 7) sb.Append('/');
|
||||
}
|
||||
|
||||
// ----- 2) active colour -----
|
||||
sb.Append(activeColour == PieceColor.White ? " w " : " b ");
|
||||
/* 2) active colour */
|
||||
sb.Append(gs.CurrentPlayer == PieceColor.White ? " w " : " b ");
|
||||
|
||||
// ----- 3) castling rights (simple check of corner rooks + kings) -----
|
||||
sb.Append(GetCastlingFlags(board));
|
||||
/* 3) castling rights (from GameState flags) */
|
||||
sb.Append(GetCastlingFlags(gs));
|
||||
|
||||
// ----- 4-6) en-passant, half-move, full-move -----
|
||||
sb.Append(" - 0 1"); // en-passant target; clocks
|
||||
/* 4) en-passant target square */
|
||||
sb.Append(' ');
|
||||
sb.Append(gs.EnPassantTarget.HasValue
|
||||
? Alg(gs.EnPassantTarget.Value)
|
||||
: "-");
|
||||
|
||||
/* 5-6) half-move clock + full-move number */
|
||||
int fullMoves = gs.MoveHistory.Count / 2 + 1;
|
||||
sb.Append(" 0 ").Append(fullMoves);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
private static string GetCastlingFlags(GameState gs)
|
||||
{
|
||||
var flags = new StringBuilder(4);
|
||||
|
||||
if (gs.WhiteCanCastleKingside) flags.Append('K');
|
||||
if (gs.WhiteCanCastleQueenside) flags.Append('Q');
|
||||
if (gs.BlackCanCastleKingside) flags.Append('k');
|
||||
if (gs.BlackCanCastleQueenside) flags.Append('q');
|
||||
|
||||
return flags.Length == 0 ? "-" : flags.ToString();
|
||||
}
|
||||
|
||||
private static string Alg(Position p)
|
||||
{
|
||||
char file = (char)('a' + p.Col);
|
||||
int rank = 8 - p.Row;
|
||||
return $"{file}{rank}";
|
||||
}
|
||||
|
||||
private static char ToFenChar(ChessPiece p) => p switch
|
||||
{
|
||||
{ Type: PieceType.Pawn, Color: PieceColor.White } => 'P',
|
||||
@@ -154,26 +228,4 @@ public static class StockfishHelpers
|
||||
{ Type: PieceType.King, Color: PieceColor.Black } => 'k',
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(p))
|
||||
};
|
||||
|
||||
private static string GetCastlingFlags(ChessPiece?[,] b)
|
||||
{
|
||||
// Fast lookup helpers
|
||||
ChessPiece? A1 = b[7, 0], H1 = b[7, 7], E1 = b[7, 4];
|
||||
ChessPiece? A8 = b[0, 0], H8 = b[0, 7], E8 = b[0, 4];
|
||||
|
||||
var flags = new StringBuilder(4);
|
||||
|
||||
if (E1 is { Type: PieceType.King, Color: PieceColor.White, HasMoved: false })
|
||||
{
|
||||
if (H1 is { Type: PieceType.Rook, Color: PieceColor.White, HasMoved: false }) flags.Append('K');
|
||||
if (A1 is { Type: PieceType.Rook, Color: PieceColor.White, HasMoved: false }) flags.Append('Q');
|
||||
}
|
||||
if (E8 is { Type: PieceType.King, Color: PieceColor.Black, HasMoved: false })
|
||||
{
|
||||
if (H8 is { Type: PieceType.Rook, Color: PieceColor.Black, HasMoved: false }) flags.Append('k');
|
||||
if (A8 is { Type: PieceType.Rook, Color: PieceColor.Black, HasMoved: false }) flags.Append('q');
|
||||
}
|
||||
|
||||
return flags.Length == 0 ? "-" : flags.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace JoshHeaps.Net.Services.Interfaces;
|
||||
|
||||
public interface IBackgroundTaskQueue
|
||||
{
|
||||
void Queue(Func<Task> workItem);
|
||||
}
|
||||
@@ -14,11 +14,11 @@
|
||||
}
|
||||
|
||||
.chessSquare.light {
|
||||
background-color: #ccc;
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.chessSquare.dark {
|
||||
background-color: #656770;
|
||||
background: #656770;
|
||||
}
|
||||
|
||||
.chessPiece {
|
||||
@@ -49,16 +49,20 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.chessSquare.previous-start {
|
||||
box-sizing: border-box;
|
||||
border: 4px solid #ffd700; /* gold or whatever you like */
|
||||
z-index: 1;
|
||||
.chessSquare.light.previous-start {
|
||||
background: linear-gradient(rgba(0, 88, 171, 0.3), rgba(0, 88, 171, 0.3)), #ccc;
|
||||
}
|
||||
|
||||
.chessSquare.previous-end {
|
||||
box-sizing: border-box;
|
||||
border: 4px solid #ff8c00; /* orange */
|
||||
z-index: 1;
|
||||
.chessSquare.light.previous-end {
|
||||
background: linear-gradient(rgba(0, 88, 171, 0.3), rgba(0, 88, 171, 0.3)), #ccc;
|
||||
}
|
||||
|
||||
.chessSquare.dark.previous-start {
|
||||
background: linear-gradient(rgba(0, 88, 171, 0.3), rgba(0, 88, 171, 0.3)), #656770;
|
||||
}
|
||||
|
||||
.chessSquare.dark.previous-end {
|
||||
background: linear-gradient(rgba(0, 88, 171, 0.3), rgba(0, 88, 171, 0.3)), #656770;
|
||||
}
|
||||
|
||||
#promotionModal {
|
||||
@@ -106,4 +110,52 @@
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
pointer-events: none; /* ensures img doesn't steal the click */
|
||||
}
|
||||
|
||||
#difficultyModal {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #1e1e1e;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.6);
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#difficultyModal p {
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#difficultyButtonContainer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#difficultyButtonContainer button {
|
||||
background-color: #2c2c2c;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#difficultyButtonContainer button:hover {
|
||||
transform: scale(1.1);
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
|
||||
#difficultyButtonContainer img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
pointer-events: none; /* ensures img doesn't steal the click */
|
||||
}
|
||||
@@ -14,6 +14,18 @@
|
||||
cursor: pointer;
|
||||
order: 1;
|
||||
padding: 2vh;
|
||||
margin: 2vh;
|
||||
}
|
||||
|
||||
#startCPUGame {
|
||||
background-color: #8cd5ed;
|
||||
color: #262626;
|
||||
border-radius: 30px;
|
||||
border: 0px;
|
||||
cursor: pointer;
|
||||
order: 1;
|
||||
padding: 2vh;
|
||||
margin: 2vh;
|
||||
}
|
||||
|
||||
#chessContainer {
|
||||
@@ -50,7 +62,8 @@
|
||||
flex-direction: column;
|
||||
order: -1;
|
||||
flex-shrink: 1;
|
||||
font-size: large
|
||||
font-size: large;
|
||||
margin: 5vw;
|
||||
}
|
||||
|
||||
#buttonContainer {
|
||||
@@ -105,10 +118,20 @@
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
#startCPUGame {
|
||||
padding: 1vw;
|
||||
margin: 1vw;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
#chessBoard {
|
||||
aspect-ratio: 1 / 1;
|
||||
flex-shrink: 1;
|
||||
width: min(90vh, 90vw);
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
#difficultyButtonContainer {
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,38 @@ async function startNewGame() {
|
||||
renderPieces(data.pieces);
|
||||
}
|
||||
|
||||
async function startCPUGame() {
|
||||
if (signalRConnection) {
|
||||
await signalRConnection.stop();
|
||||
signalRConnection = null;
|
||||
}
|
||||
|
||||
let difficulty = await promptDifficulty();
|
||||
|
||||
// Join the game via API
|
||||
const response = await fetch(`/api/chess/new/${difficulty}`);
|
||||
const gameData = await response.json();
|
||||
|
||||
currentGameId = gameData.gameId;
|
||||
currentPlayerId = gameData.id;
|
||||
currentPlayerIsWhite = gameData.isWhite;
|
||||
previousMoveStart = null;
|
||||
previousMoveEnd = null;
|
||||
document.cookie = `chessGameId=${currentGameId}; path=/; max-age=86400`; // expires in 1 day
|
||||
document.cookie = `chessPlayerId=${currentPlayerId}; path=/; max-age=86400`;
|
||||
document.cookie = `chessPlayerIsWhite=${currentPlayerIsWhite}; path=/; max-age=86400`
|
||||
console.log("🆕 Game started:", currentGameId);
|
||||
|
||||
// Build and start SignalR connection
|
||||
await setupSignalRConnection();
|
||||
|
||||
// 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++) {
|
||||
@@ -58,6 +90,28 @@ function renderPieces(pieces) {
|
||||
img.alt = piece.type;
|
||||
img.classList.add("chessPiece");
|
||||
|
||||
img.draggable = true;
|
||||
img.ondragstart = async (e) => {
|
||||
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(err);
|
||||
}
|
||||
|
||||
e.dataTransfer.setData("text/plain", JSON.stringify({
|
||||
srcRow: piece.Row,
|
||||
srcCol: piece.Col
|
||||
}))
|
||||
}
|
||||
|
||||
img.ondragend = clearHighlights;
|
||||
|
||||
img.onclick = (e) => {
|
||||
e.stopPropagation(); // 👈 Prevents the parent square click from firing
|
||||
handlePieceClick(piece);
|
||||
@@ -310,6 +364,16 @@ function promptPromotion() {
|
||||
});
|
||||
}
|
||||
|
||||
function promptDifficulty() {
|
||||
return new Promise(resolve => {
|
||||
document.getElementById("difficultyModal").style.display = "block";
|
||||
window.selectDifficulty = (difficulty) => {
|
||||
document.getElementById("difficultyModal").style.display = "none";
|
||||
resolve(difficulty);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updatePromotionModalImages(color) {
|
||||
const pieceNames = ["Queen", "Rook", "Bishop", "Knight"];
|
||||
const buttons = document.querySelectorAll("#promotionModal button img");
|
||||
@@ -346,4 +410,30 @@ window.addEventListener('load', async () => {
|
||||
console.error("Failed to rejoin saved game:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// add near the bottom of chessLogic.js, run once after the DOM is ready
|
||||
for (let i = 0; i < 64; i++) {
|
||||
const square = document.getElementById(`square-${i}`);
|
||||
|
||||
// Allow dropping by cancelling the default
|
||||
square.ondragover = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
square.ondrop = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// If no piece is being dragged, ignore
|
||||
if (!selectedPiece) return;
|
||||
|
||||
// Board-space index to (row,col)
|
||||
const index = parseInt(square.id.split('-')[1], 10);
|
||||
const targetRow = Math.floor(index / 8);
|
||||
const targetCol = index % 8;
|
||||
|
||||
handleMove(targetRow, targetCol);
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user