265 lines
8.3 KiB
C#
265 lines
8.3 KiB
C#
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(
|
|
IChessService chessService,
|
|
IHubContext<ChessHub> chessHub,
|
|
IBackgroundTaskQueue queue) : ControllerBase
|
|
{
|
|
/// <summary>
|
|
/// Store of ongoing games.
|
|
/// </summary>
|
|
private static readonly ConcurrentDictionary<Guid, GameState> _games = [];
|
|
private static readonly ConcurrentDictionary<Guid, Task> _gameRemovalTasks = [];
|
|
private static readonly ConcurrentDictionary<Guid, CancellationTokenSource> _gameRemovalCancellationTokens = [];
|
|
|
|
private static readonly TimeSpan _computerGameTimeout = TimeSpan.FromHours(1);
|
|
private static readonly TimeSpan _multiplayerGameTimeout = TimeSpan.FromSeconds(5);
|
|
private static readonly TimeSpan _gameCleanupTimeout = TimeSpan.FromMinutes(1);
|
|
|
|
/// <summary>
|
|
/// Create a new chess game and store it in-memory.
|
|
/// </summary>
|
|
[HttpGet("new")]
|
|
[HttpGet("new/{difficulty}")]
|
|
public ActionResult CreateGame(int difficulty = 20)
|
|
{
|
|
var gameState = chessService.CreateNewGame();
|
|
_games[gameState.GameId] = gameState;
|
|
|
|
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, _computerGameTimeout);
|
|
|
|
return Ok(new
|
|
{
|
|
Id = playerId,
|
|
IsWhite = isWhite,
|
|
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;
|
|
}
|
|
|
|
ScheduleRemoveGame(gameState.GameId, _multiplayerGameTimeout);
|
|
|
|
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)
|
|
ScheduleRemoveGame(gameState.GameId, _gameCleanupTimeout);
|
|
else if (gameState.IsVsComputer)
|
|
ScheduleRemoveGame(gameState.GameId, _computerGameTimeout);
|
|
else
|
|
ScheduleRemoveGame(gameState.GameId, _multiplayerGameTimeout);
|
|
|
|
if (gameState.IsVsComputer && gameState.Computer is not null)
|
|
queue.Queue(() => gameState.Computer.MakeMove(gameState, chessHub, chessService));
|
|
|
|
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);
|
|
}
|
|
|
|
private static void ScheduleRemoveGame(Guid id, TimeSpan delay)
|
|
{
|
|
if (_gameRemovalCancellationTokens.TryRemove(id, out var oldCts))
|
|
{
|
|
oldCts.Cancel();
|
|
oldCts.Dispose();
|
|
}
|
|
|
|
var cts = new CancellationTokenSource();
|
|
_gameRemovalCancellationTokens[id] = cts;
|
|
|
|
_gameRemovalTasks[id] = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(delay, cts.Token);
|
|
|
|
if (_games.TryGetValue(id, out var game) && game.Computer is not null)
|
|
await game.Computer.DisposeAsync();
|
|
|
|
_games.Remove(id, out _);
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
finally
|
|
{
|
|
if (_gameRemovalCancellationTokens.TryGetValue(id, out var currentCts) && currentCts == cts)
|
|
{
|
|
_gameRemovalCancellationTokens.TryRemove(id, out _);
|
|
}
|
|
|
|
cts.Dispose();
|
|
}
|
|
});
|
|
}
|
|
}
|