From f7f752b694b77f1c3a8de701dcea58564aa88187 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Wed, 9 Nov 2022 18:50:51 -0600 Subject: [PATCH] started working on player moves. --- Shogi.Api/Controllers/SessionsController.cs | 196 +++++++----------- Shogi.Api/Controllers/UserController.cs | 142 ++++++------- Shogi.Api/Extensions/ContractsExtensions.cs | 16 ++ .../Api/Commands/CreateTokenResponse.cs | 15 +- .../Api/Commands/MovePieceCommand.cs | 46 +++- .../{Move.cs => PlayerHasMovedMessage.cs} | 8 +- ...Game.cs => SessionCreatedSocketMessage.cs} | 0 Shogi.Contracts/Types/Move.cs | 12 -- Shogi.Domain/Aggregates/Session.cs | 5 + Shogi.UI/Pages/Home/Account/AccountManager.cs | 4 +- Shogi.UI/Pages/Home/Api/IShogiApi.cs | 20 +- Shogi.UI/Pages/Home/Api/ShogiApi.cs | 37 +--- Shogi.UI/Pages/Home/GameBrowser.razor | 2 +- 13 files changed, 232 insertions(+), 271 deletions(-) rename Shogi.Contracts/Socket/{Move.cs => PlayerHasMovedMessage.cs} (55%) rename Shogi.Contracts/Socket/{CreateGame.cs => SessionCreatedSocketMessage.cs} (100%) delete mode 100644 Shogi.Contracts/Types/Move.cs diff --git a/Shogi.Api/Controllers/SessionsController.cs b/Shogi.Api/Controllers/SessionsController.cs index 516ec7a..92b7b30 100644 --- a/Shogi.Api/Controllers/SessionsController.cs +++ b/Shogi.Api/Controllers/SessionsController.cs @@ -68,7 +68,82 @@ public class SessionsController : ControllerBase return this.NoContent(); } - return this.Unauthorized("Cannot delete sessions created by others."); + return this.Forbid("Cannot delete sessions created by others."); + } + + [HttpGet("PlayerCount")] + public async Task> GetSessionsPlayerCount() + { + var sessions = await this.queryRespository.ReadSessionPlayerCount(); + + return Ok(new ReadSessionsPlayerCountResponse + { + PlayerHasJoinedSessions = Array.Empty(), + AllOtherSessions = sessions.ToList() + }); + } + + [HttpGet("{name}")] + public async Task> GetSession(string name) + { + var session = await sessionRepository.ReadSession(name); + if (session == null) return this.NotFound(); + + return new ReadSessionResponse + { + Session = new Session + { + BoardState = new BoardState + { + Board = session.Board.BoardState.State.ToContract(), + Player1Hand = session.Board.BoardState.Player1Hand.ToContract(), + Player2Hand = session.Board.BoardState.Player2Hand.ToContract(), + PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(), + WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract() + }, + Player1 = session.Player1, + Player2 = session.Player2, + SessionName = session.Name + } + }; + } + + [HttpPatch("{name}/Move")] + public async Task Move([FromRoute] string name, [FromBody] MovePieceCommand command) + { + var userId = User.GetShogiUserId(); + var session = await sessionRepository.ReadSession(name); + + if (session == null) return this.NotFound("Shogi session does not exist."); + + if (!session.IsSeated(userId)) return this.Forbid("Player is not a member of the Shogi session."); + + try + { + if (command.PieceFromHand.HasValue) + { + session.Board.Move(command.PieceFromHand.Value.ToDomain(), command.To); + } + else + { + session.Board.Move(command.From!, command.To, command.IsPromotion); + } + } + catch (InvalidOperationException) + { + return this.Conflict("Move is illegal."); + } + // TODO: sessionRespository.SaveMove(); + await communicationManager.BroadcastToPlayers( + new PlayerHasMovedMessage + { + PlayerName = userId, + SessionName = session.Name, + }, + session.Player1, + session.Player2); + + return this.NoContent(); } //[HttpPost("{sessionName}/Move")] @@ -112,100 +187,6 @@ public class SessionsController : ControllerBase // } //} - // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. - //[Route("")] - //public async Task PostSession([FromBody] PostSession request) - //{ - // var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2); - // var success = await repository.CreateSession(model); - // if (success) - // { - // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.SocketAction.CreateGame) - // { - // Game = model.ToServiceModel(), - // PlayerName = - // } - // var task = request.IsPrivate - // ? communicationManager.BroadcastToPlayers(response, userName) - // : communicationManager.BroadcastToAll(response); - // return new CreatedResult("", null); - // } - // return new ConflictResult(); - //} - - - - //[HttpGet("{sessionName}")] - //[AllowAnonymous] - //public async Task GetSession([FromRoute] string sessionName) - //{ - // var user = await ReadUserOrThrow(); - // var session = await gameboardRepository.ReadSession(sessionName); - // if (session == null) - // { - // return NotFound(); - // } - - // var playerPerspective = session.Player2 == user.Id - // ? WhichPlayer.Player2 - // : WhichPlayer.Player1; - - // var response = new ReadSessionResponse - // { - // Session = new Session - // { - // BoardState = new BoardState - // { - // Board = mapper.Map(session.BoardState.State), - // Player1Hand = session.BoardState.Player1Hand.Select(mapper.Map).ToList(), - // Player2Hand = session.BoardState.Player2Hand.Select(mapper.Map).ToList(), - // PlayerInCheck = mapper.Map(session.BoardState.InCheck) - // }, - // SessionName = session.Name, - // Player1 = session.Player1, - // Player2 = session.Player2 - // } - // }; - // return Ok(response); - //} - - [HttpGet("PlayerCount")] - public async Task> GetSessionsPlayerCount() - { - var sessions = await this.queryRespository.ReadSessionPlayerCount(); - - return Ok(new ReadSessionsPlayerCountResponse - { - PlayerHasJoinedSessions = Array.Empty(), - AllOtherSessions = sessions.ToList() - }); - } - - [HttpGet("{name}")] - public async Task> GetSession(string name) - { - var session = await sessionRepository.ReadSession(name); - if (session == null) return this.NotFound(); - - return new ReadSessionResponse - { - Session = new Session - { - BoardState = new BoardState - { - Board = session.Board.BoardState.State.ToContract(), - Player1Hand = session.Board.BoardState.Player1Hand.ToContract(), - Player2Hand = session.Board.BoardState.Player2Hand.ToContract(), - PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(), - WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract() - }, - Player1 = session.Player1, - Player2 = session.Player2, - SessionName = session.Name - } - }; - } - //[HttpPut("{sessionName}")] //public async Task PutJoinSession([FromRoute] string sessionName) //{ @@ -233,29 +214,4 @@ public class SessionsController : ControllerBase // }, opponentName); // return Ok(); //} - - //[Authorize(Roles = "Admin")] - //[HttpDelete("{sessionName}")] - //public async Task DeleteSession([FromRoute] string sessionName) - //{ - // var user = await ReadUserOrThrow(); - // if (user.IsAdmin) - // { - // return Ok(); - // } - // else - // { - // return Unauthorized(); - // } - //} - - //private async Task ReadUserOrThrow() - //{ - // var user = await gameboardManager.ReadUser(User); - // if (user == null) - // { - // throw new UnauthorizedAccessException("Unknown user claims."); - // } - // return user; - //} } diff --git a/Shogi.Api/Controllers/UserController.cs b/Shogi.Api/Controllers/UserController.cs index 310d359..10292ad 100644 --- a/Shogi.Api/Controllers/UserController.cs +++ b/Shogi.Api/Controllers/UserController.cs @@ -14,91 +14,73 @@ namespace Shogi.Api.Controllers; [Authorize] public class UserController : ControllerBase { - private readonly ISocketTokenCache tokenCache; - private readonly ISocketConnectionManager connectionManager; - private readonly IUserRepository userRepository; - private readonly IShogiUserClaimsTransformer claimsTransformation; - private readonly AuthenticationProperties authenticationProps; + private readonly ISocketTokenCache tokenCache; + private readonly ISocketConnectionManager connectionManager; + private readonly IUserRepository userRepository; + private readonly IShogiUserClaimsTransformer claimsTransformation; + private readonly AuthenticationProperties authenticationProps; - public UserController( - ILogger logger, - ISocketTokenCache tokenCache, - ISocketConnectionManager connectionManager, - IUserRepository userRepository, - IShogiUserClaimsTransformer claimsTransformation) - { - this.tokenCache = tokenCache; - this.connectionManager = connectionManager; - this.userRepository = userRepository; - this.claimsTransformation = claimsTransformation; - authenticationProps = new AuthenticationProperties - { - AllowRefresh = true, - IsPersistent = true - }; - } + public UserController( + ILogger logger, + ISocketTokenCache tokenCache, + ISocketConnectionManager connectionManager, + IUserRepository userRepository, + IShogiUserClaimsTransformer claimsTransformation) + { + this.tokenCache = tokenCache; + this.connectionManager = connectionManager; + this.userRepository = userRepository; + this.claimsTransformation = claimsTransformation; + authenticationProps = new AuthenticationProperties + { + AllowRefresh = true, + IsPersistent = true + }; + } - [HttpPut("GuestLogout")] - public async Task GuestLogout() - { - var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + [HttpPut("GuestLogout")] + public async Task GuestLogout() + { + var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - var userId = User?.GetGuestUserId(); - if (!string.IsNullOrEmpty(userId)) - { - connectionManager.Unsubscribe(userId); - } + var userId = User?.GetGuestUserId(); + if (!string.IsNullOrEmpty(userId)) + { + connectionManager.Unsubscribe(userId); + } - await signoutTask; - return Ok(); - } + await signoutTask; + return Ok(); + } - //[HttpGet("Token")] - //public async Task GetToken() - //{ - // var user = await gameboardManager.ReadUser(User); - // if (user == null) - // { - // await gameboardManager.CreateUser(User); - // user = await gameboardManager.ReadUser(User); - // } + [HttpGet("Token")] + public ActionResult GetToken() + { + var userId = User.GetShogiUserId(); + var displayName = User.DisplayName(); - // if (user == null) - // { - // return Unauthorized(); - // } + var token = tokenCache.GenerateToken(userId); + return new CreateTokenResponse + { + DisplayName = displayName, + OneTimeToken = token, + UserId = userId + }; + } - // var token = tokenCache.GenerateToken(user.Id); - // return new JsonResult(new CreateTokenResponse(token)); - //} - - [AllowAnonymous] - [HttpGet("LoginAsGuest")] - public async Task GuestLogin() - { - var principal = await this.claimsTransformation.CreateClaimsFromGuestPrincipal(User); - if (principal != null) - { - await HttpContext.SignInAsync( - CookieAuthenticationDefaults.AuthenticationScheme, - principal, - authenticationProps - ); - } - return Ok(); - } - - [HttpGet("GuestToken")] - public IActionResult GetGuestToken() - { - var id = User.GetGuestUserId(); - var displayName = User.DisplayName(); - if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(displayName)) - { - var token = tokenCache.GenerateToken(User.GetGuestUserId()!); - return this.Ok(new CreateGuestTokenResponse(id, displayName, token)); - } - - return this.Unauthorized(); - } + [AllowAnonymous] + [HttpGet("LoginAsGuest")] + public async Task GuestLogin() + { + var principal = await this.claimsTransformation.CreateClaimsFromGuestPrincipal(User); + if (principal != null) + { + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + principal, + authenticationProps + ); + } + return Ok(); + } } diff --git a/Shogi.Api/Extensions/ContractsExtensions.cs b/Shogi.Api/Extensions/ContractsExtensions.cs index 0e8945f..472da1f 100644 --- a/Shogi.Api/Extensions/ContractsExtensions.cs +++ b/Shogi.Api/Extensions/ContractsExtensions.cs @@ -49,4 +49,20 @@ public static class ContractsExtensions public static Dictionary ToContract(this ReadOnlyDictionary boardState) => boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract()); + + public static Domain.WhichPiece ToDomain(this WhichPiece piece) + { + return piece switch + { + WhichPiece.King => Domain.WhichPiece.King, + WhichPiece.GoldGeneral => Domain.WhichPiece.GoldGeneral, + WhichPiece.SilverGeneral => Domain.WhichPiece.SilverGeneral, + WhichPiece.Bishop => Domain.WhichPiece.Bishop, + WhichPiece.Rook => Domain.WhichPiece.Rook, + WhichPiece.Knight => Domain.WhichPiece.Knight, + WhichPiece.Lance => Domain.WhichPiece.Lance, + WhichPiece.Pawn => Domain.WhichPiece.Pawn, + _ => throw new NotImplementedException(), + }; + } } diff --git a/Shogi.Contracts/Api/Commands/CreateTokenResponse.cs b/Shogi.Contracts/Api/Commands/CreateTokenResponse.cs index feea0a3..eb141dc 100644 --- a/Shogi.Contracts/Api/Commands/CreateTokenResponse.cs +++ b/Shogi.Contracts/Api/Commands/CreateTokenResponse.cs @@ -2,12 +2,9 @@ namespace Shogi.Contracts.Api; - public class CreateTokenResponse - { - public Guid OneTimeToken { get; } - - public CreateTokenResponse(Guid token) - { - OneTimeToken = token; - } - } +public class CreateTokenResponse +{ + public string UserId { get; set; } + public string DisplayName { get; set; } + public Guid OneTimeToken { get; set; } +} diff --git a/Shogi.Contracts/Api/Commands/MovePieceCommand.cs b/Shogi.Contracts/Api/Commands/MovePieceCommand.cs index aa9082e..56a94d8 100644 --- a/Shogi.Contracts/Api/Commands/MovePieceCommand.cs +++ b/Shogi.Contracts/Api/Commands/MovePieceCommand.cs @@ -1,10 +1,46 @@ using Shogi.Contracts.Types; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; namespace Shogi.Contracts.Api; - public class MovePieceCommand - { - [Required] - public Move Move { get; set; } - } +public class MovePieceCommand : IValidatableObject +{ + /// + /// Mutually exclusive with . + /// Set this property to indicate moving a piece from the hand onto the board. + /// + public WhichPiece? PieceFromHand { get; set; } + + /// + /// Board position notation, like A3 or G1 + /// Mutually exclusive with . + /// Set this property to indicate moving a piece from the board to another position on the board. + /// + public string? From { get; set; } + + /// + /// Board position notation, like A3 or G1 + /// + [Required] + public string To { get; set; } + + public bool IsPromotion { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (PieceFromHand.HasValue && !string.IsNullOrWhiteSpace(From)) + { + yield return new ValidationResult($"{nameof(PieceFromHand)} and {nameof(From)} are mutually exclusive properties."); + } + if (!Regex.IsMatch(To, "[A-I][1-9]")) + { + yield return new ValidationResult($"{nameof(To)} must be a valid board position, between A1 and I9"); + } + if (!string.IsNullOrEmpty(From) && !Regex.IsMatch(From, "[A-I][1-9]")) + { + yield return new ValidationResult($"{nameof(From)} must be a valid board position, between A1 and I9"); + } + } +} diff --git a/Shogi.Contracts/Socket/Move.cs b/Shogi.Contracts/Socket/PlayerHasMovedMessage.cs similarity index 55% rename from Shogi.Contracts/Socket/Move.cs rename to Shogi.Contracts/Socket/PlayerHasMovedMessage.cs index 8dcc72e..c55ccdd 100644 --- a/Shogi.Contracts/Socket/Move.cs +++ b/Shogi.Contracts/Socket/PlayerHasMovedMessage.cs @@ -2,16 +2,16 @@ namespace Shogi.Contracts.Socket; -public class MoveResponse : ISocketResponse +public class PlayerHasMovedMessage : ISocketResponse { public SocketAction Action { get; } - public string SessionName { get; set; } = string.Empty; + public string SessionName { get; set; } /// /// The player that made the move. /// - public string PlayerName { get; set; } = string.Empty; + public string PlayerName { get; set; } - public MoveResponse() + public PlayerHasMovedMessage() { Action = SocketAction.PieceMoved; } diff --git a/Shogi.Contracts/Socket/CreateGame.cs b/Shogi.Contracts/Socket/SessionCreatedSocketMessage.cs similarity index 100% rename from Shogi.Contracts/Socket/CreateGame.cs rename to Shogi.Contracts/Socket/SessionCreatedSocketMessage.cs diff --git a/Shogi.Contracts/Types/Move.cs b/Shogi.Contracts/Types/Move.cs deleted file mode 100644 index ef72029..0000000 --- a/Shogi.Contracts/Types/Move.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Shogi.Contracts.Types -{ - public class Move - { - public WhichPiece? PieceFromCaptured { get; set; } - /// Board position notation, like A3 or G1 - public string? From { get; set; } - /// Board position notation, like A3 or G1 - public string To { get; set; } = string.Empty; - public bool IsPromotion { get; set; } - } -} diff --git a/Shogi.Domain/Aggregates/Session.cs b/Shogi.Domain/Aggregates/Session.cs index 2a817cf..3309ebf 100644 --- a/Shogi.Domain/Aggregates/Session.cs +++ b/Shogi.Domain/Aggregates/Session.cs @@ -23,4 +23,9 @@ public class Session if (Player2 != null) throw new InvalidOperationException("Player 2 already exists while trying to add a second player."); Player2 = player2Name; } + + public bool IsSeated(string playerName) + { + return Player1 == playerName || Player2 == playerName; + } } diff --git a/Shogi.UI/Pages/Home/Account/AccountManager.cs b/Shogi.UI/Pages/Home/Account/AccountManager.cs index a6c9afd..f77c7d6 100644 --- a/Shogi.UI/Pages/Home/Account/AccountManager.cs +++ b/Shogi.UI/Pages/Home/Account/AccountManager.cs @@ -36,7 +36,7 @@ public class AccountManager public async Task LoginWithGuestAccount() { - var response = await shogiApi.GetGuestToken(); + var response = await shogiApi.GetToken(); if (response != null) { User = new User @@ -87,7 +87,7 @@ public class AccountManager var platform = await localStorage.GetAccountPlatform(); if (platform == WhichAccountPlatform.Guest) { - var response = await shogiApi.GetGuestToken(); + var response = await shogiApi.GetToken(); if (response != null) { User = new User diff --git a/Shogi.UI/Pages/Home/Api/IShogiApi.cs b/Shogi.UI/Pages/Home/Api/IShogiApi.cs index 5773fd4..16b89f3 100644 --- a/Shogi.UI/Pages/Home/Api/IShogiApi.cs +++ b/Shogi.UI/Pages/Home/Api/IShogiApi.cs @@ -2,16 +2,14 @@ using Shogi.Contracts.Types; using System.Net; -namespace Shogi.UI.Pages.Home.Api +namespace Shogi.UI.Pages.Home.Api; + +public interface IShogiApi { - public interface IShogiApi - { - Task GetGuestToken(); - Task GetSession(string name); - Task GetSessions(); - Task GetToken(); - Task GuestLogout(); - Task PostMove(string sessionName, Move move); - Task PostSession(string name, bool isPrivate); - } + Task GetSession(string name); + Task GetSessionsPlayerCount(); + Task GetToken(); + Task GuestLogout(); + Task PostMove(string sessionName, Move move); + Task PostSession(string name, bool isPrivate); } \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Api/ShogiApi.cs b/Shogi.UI/Pages/Home/Api/ShogiApi.cs index ef2f73f..19f1e98 100644 --- a/Shogi.UI/Pages/Home/Api/ShogiApi.cs +++ b/Shogi.UI/Pages/Home/Api/ShogiApi.cs @@ -4,11 +4,10 @@ using Shogi.UI.Pages.Home.Account; using System.Net; using System.Net.Http.Json; using System.Text.Json; -using System.Text.Json.Serialization; namespace Shogi.UI.Pages.Home.Api { - public class ShogiApi : IShogiApi + public class ShogiApi : IShogiApi { public const string GuestClientName = "Guest"; public const string MsalClientName = "Msal"; @@ -20,12 +19,7 @@ namespace Shogi.UI.Pages.Home.Api public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState) { - serializerOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - }; - serializerOptions.Converters.Add(new JsonStringEnumConverter()); + serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); this.clientFactory = clientFactory; this.accountState = accountState; } @@ -37,16 +31,6 @@ namespace Shogi.UI.Pages.Home.Api _ => clientFactory.CreateClient(AnonymouseClientName) }; - public async Task GetGuestToken() - { - var response = await HttpClient.GetAsync(new Uri("User/GuestToken", UriKind.Relative)); - if (response.IsSuccessStatusCode) - { - return await response.Content.ReadFromJsonAsync(serializerOptions); - } - return null; - } - public async Task GuestLogout() { var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null); @@ -55,7 +39,7 @@ namespace Shogi.UI.Pages.Home.Api public async Task GetSession(string name) { - var response = await HttpClient.GetAsync(new Uri($"Session/{name}", UriKind.Relative)); + var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative)); if (response.IsSuccessStatusCode) { return (await response.Content.ReadFromJsonAsync(serializerOptions))?.Session; @@ -63,9 +47,9 @@ namespace Shogi.UI.Pages.Home.Api return null; } - public async Task GetSessions() + public async Task GetSessionsPlayerCount() { - var response = await HttpClient.GetAsync(new Uri("Session", UriKind.Relative)); + var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative)); if (response.IsSuccessStatusCode) { return await response.Content.ReadFromJsonAsync(serializerOptions); @@ -73,21 +57,20 @@ namespace Shogi.UI.Pages.Home.Api return null; } - public async Task GetToken() + public async Task GetToken() { - var response = await HttpClient.GetAsync(new Uri("User/Token", UriKind.Relative)); - var deserialized = await response.Content.ReadFromJsonAsync(serializerOptions); - return deserialized?.OneTimeToken; + var response = await HttpClient.GetFromJsonAsync(new Uri("User/Token", UriKind.Relative), serializerOptions); + return response; } public async Task PostMove(string sessionName, Contracts.Types.Move move) { - await this.HttpClient.PostAsJsonAsync($"{sessionName}/Move", new MovePieceCommand { Move = move }); + await this.HttpClient.PostAsJsonAsync($"Sessions{sessionName}/Move", new MovePieceCommand { Move = move }); } public async Task PostSession(string name, bool isPrivate) { - var response = await HttpClient.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand + var response = await HttpClient.PostAsJsonAsync(new Uri("Sessions", UriKind.Relative), new CreateSessionCommand { Name = name, }); diff --git a/Shogi.UI/Pages/Home/GameBrowser.razor b/Shogi.UI/Pages/Home/GameBrowser.razor index d5220ef..5030401 100644 --- a/Shogi.UI/Pages/Home/GameBrowser.razor +++ b/Shogi.UI/Pages/Home/GameBrowser.razor @@ -88,7 +88,7 @@ async Task FetchSessions() { - var sessions = await ShogiApi.GetSessions(); + var sessions = await ShogiApi.GetSessionsPlayerCount(); if (sessions != null) { this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray();