started working on player moves.

This commit is contained in:
2022-11-09 18:50:51 -06:00
parent da76917490
commit f7f752b694
13 changed files with 232 additions and 271 deletions

View File

@@ -68,7 +68,82 @@ public class SessionsController : ControllerBase
return this.NoContent(); 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<ActionResult<ReadSessionsPlayerCountResponse>> GetSessionsPlayerCount()
{
var sessions = await this.queryRespository.ReadSessionPlayerCount();
return Ok(new ReadSessionsPlayerCountResponse
{
PlayerHasJoinedSessions = Array.Empty<SessionMetadata>(),
AllOtherSessions = sessions.ToList()
});
}
[HttpGet("{name}")]
public async Task<ActionResult<ReadSessionResponse>> 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<IActionResult> 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")] //[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<IActionResult> 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<IActionResult> 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<ActionResult<ReadSessionsPlayerCountResponse>> GetSessionsPlayerCount()
{
var sessions = await this.queryRespository.ReadSessionPlayerCount();
return Ok(new ReadSessionsPlayerCountResponse
{
PlayerHasJoinedSessions = Array.Empty<SessionMetadata>(),
AllOtherSessions = sessions.ToList()
});
}
[HttpGet("{name}")]
public async Task<ActionResult<ReadSessionResponse>> 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}")] //[HttpPut("{sessionName}")]
//public async Task<IActionResult> PutJoinSession([FromRoute] string sessionName) //public async Task<IActionResult> PutJoinSession([FromRoute] string sessionName)
//{ //{
@@ -233,29 +214,4 @@ public class SessionsController : ControllerBase
// }, opponentName); // }, opponentName);
// return Ok(); // return Ok();
//} //}
//[Authorize(Roles = "Admin")]
//[HttpDelete("{sessionName}")]
//public async Task<IActionResult> DeleteSession([FromRoute] string sessionName)
//{
// var user = await ReadUserOrThrow();
// if (user.IsAdmin)
// {
// return Ok();
// }
// else
// {
// return Unauthorized();
// }
//}
//private async Task<Models.User> ReadUserOrThrow()
//{
// var user = await gameboardManager.ReadUser(User);
// if (user == null)
// {
// throw new UnauthorizedAccessException("Unknown user claims.");
// }
// return user;
//}
} }

View File

@@ -53,24 +53,20 @@ public class UserController : ControllerBase
return Ok(); return Ok();
} }
//[HttpGet("Token")] [HttpGet("Token")]
//public async Task<IActionResult> GetToken() public ActionResult<CreateTokenResponse> GetToken()
//{ {
// var user = await gameboardManager.ReadUser(User); var userId = User.GetShogiUserId();
// if (user == null) var displayName = User.DisplayName();
// {
// await gameboardManager.CreateUser(User);
// user = await gameboardManager.ReadUser(User);
// }
// if (user == null) var token = tokenCache.GenerateToken(userId);
// { return new CreateTokenResponse
// return Unauthorized(); {
// } DisplayName = displayName,
OneTimeToken = token,
// var token = tokenCache.GenerateToken(user.Id); UserId = userId
// return new JsonResult(new CreateTokenResponse(token)); };
//} }
[AllowAnonymous] [AllowAnonymous]
[HttpGet("LoginAsGuest")] [HttpGet("LoginAsGuest")]
@@ -87,18 +83,4 @@ public class UserController : ControllerBase
} }
return Ok(); 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();
}
} }

View File

@@ -49,4 +49,20 @@ public static class ContractsExtensions
public static Dictionary<string, Piece?> ToContract(this ReadOnlyDictionary<string, Domain.ValueObjects.Piece?> boardState) => public static Dictionary<string, Piece?> ToContract(this ReadOnlyDictionary<string, Domain.ValueObjects.Piece?> boardState) =>
boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract()); 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(),
};
}
} }

View File

@@ -2,12 +2,9 @@
namespace Shogi.Contracts.Api; namespace Shogi.Contracts.Api;
public class CreateTokenResponse public class CreateTokenResponse
{ {
public Guid OneTimeToken { get; } public string UserId { get; set; }
public string DisplayName { get; set; }
public CreateTokenResponse(Guid token) public Guid OneTimeToken { get; set; }
{ }
OneTimeToken = token;
}
}

View File

@@ -1,10 +1,46 @@
using Shogi.Contracts.Types; using Shogi.Contracts.Types;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace Shogi.Contracts.Api; namespace Shogi.Contracts.Api;
public class MovePieceCommand public class MovePieceCommand : IValidatableObject
{ {
/// <summary>
/// Mutually exclusive with <see cref="From"/>.
/// Set this property to indicate moving a piece from the hand onto the board.
/// </summary>
public WhichPiece? PieceFromHand { get; set; }
/// <summary>
/// Board position notation, like A3 or G1
/// Mutually exclusive with <see cref="PieceFromHand"/>.
/// Set this property to indicate moving a piece from the board to another position on the board.
/// </summary>
public string? From { get; set; }
/// <summary>
/// Board position notation, like A3 or G1
/// </summary>
[Required] [Required]
public Move Move { get; set; } public string To { get; set; }
public bool IsPromotion { get; set; }
public IEnumerable<ValidationResult> 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");
}
}
}

View File

@@ -2,16 +2,16 @@
namespace Shogi.Contracts.Socket; namespace Shogi.Contracts.Socket;
public class MoveResponse : ISocketResponse public class PlayerHasMovedMessage : ISocketResponse
{ {
public SocketAction Action { get; } public SocketAction Action { get; }
public string SessionName { get; set; } = string.Empty; public string SessionName { get; set; }
/// <summary> /// <summary>
/// The player that made the move. /// The player that made the move.
/// </summary> /// </summary>
public string PlayerName { get; set; } = string.Empty; public string PlayerName { get; set; }
public MoveResponse() public PlayerHasMovedMessage()
{ {
Action = SocketAction.PieceMoved; Action = SocketAction.PieceMoved;
} }

View File

@@ -1,12 +0,0 @@
namespace Shogi.Contracts.Types
{
public class Move
{
public WhichPiece? PieceFromCaptured { get; set; }
/// <summary>Board position notation, like A3 or G1</summary>
public string? From { get; set; }
/// <summary>Board position notation, like A3 or G1</summary>
public string To { get; set; } = string.Empty;
public bool IsPromotion { get; set; }
}
}

View File

@@ -23,4 +23,9 @@ public class Session
if (Player2 != null) throw new InvalidOperationException("Player 2 already exists while trying to add a second player."); if (Player2 != null) throw new InvalidOperationException("Player 2 already exists while trying to add a second player.");
Player2 = player2Name; Player2 = player2Name;
} }
public bool IsSeated(string playerName)
{
return Player1 == playerName || Player2 == playerName;
}
} }

View File

@@ -36,7 +36,7 @@ public class AccountManager
public async Task LoginWithGuestAccount() public async Task LoginWithGuestAccount()
{ {
var response = await shogiApi.GetGuestToken(); var response = await shogiApi.GetToken();
if (response != null) if (response != null)
{ {
User = new User User = new User
@@ -87,7 +87,7 @@ public class AccountManager
var platform = await localStorage.GetAccountPlatform(); var platform = await localStorage.GetAccountPlatform();
if (platform == WhichAccountPlatform.Guest) if (platform == WhichAccountPlatform.Guest)
{ {
var response = await shogiApi.GetGuestToken(); var response = await shogiApi.GetToken();
if (response != null) if (response != null)
{ {
User = new User User = new User

View File

@@ -2,16 +2,14 @@
using Shogi.Contracts.Types; using Shogi.Contracts.Types;
using System.Net; using System.Net;
namespace Shogi.UI.Pages.Home.Api namespace Shogi.UI.Pages.Home.Api;
public interface IShogiApi
{ {
public interface IShogiApi
{
Task<CreateGuestTokenResponse?> GetGuestToken();
Task<Session?> GetSession(string name); Task<Session?> GetSession(string name);
Task<ReadSessionsPlayerCountResponse?> GetSessions(); Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount();
Task<Guid?> GetToken(); Task<CreateTokenResponse?> GetToken();
Task GuestLogout(); Task GuestLogout();
Task PostMove(string sessionName, Move move); Task PostMove(string sessionName, Move move);
Task<HttpStatusCode> PostSession(string name, bool isPrivate); Task<HttpStatusCode> PostSession(string name, bool isPrivate);
}
} }

View File

@@ -4,7 +4,6 @@ using Shogi.UI.Pages.Home.Account;
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
namespace Shogi.UI.Pages.Home.Api namespace Shogi.UI.Pages.Home.Api
{ {
@@ -20,12 +19,7 @@ namespace Shogi.UI.Pages.Home.Api
public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState) public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState)
{ {
serializerOptions = new JsonSerializerOptions serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
serializerOptions.Converters.Add(new JsonStringEnumConverter());
this.clientFactory = clientFactory; this.clientFactory = clientFactory;
this.accountState = accountState; this.accountState = accountState;
} }
@@ -37,16 +31,6 @@ namespace Shogi.UI.Pages.Home.Api
_ => clientFactory.CreateClient(AnonymouseClientName) _ => clientFactory.CreateClient(AnonymouseClientName)
}; };
public async Task<CreateGuestTokenResponse?> GetGuestToken()
{
var response = await HttpClient.GetAsync(new Uri("User/GuestToken", UriKind.Relative));
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<CreateGuestTokenResponse>(serializerOptions);
}
return null;
}
public async Task GuestLogout() public async Task GuestLogout()
{ {
var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null); 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<Session?> GetSession(string name) public async Task<Session?> 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) if (response.IsSuccessStatusCode)
{ {
return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session; return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session;
@@ -63,9 +47,9 @@ namespace Shogi.UI.Pages.Home.Api
return null; return null;
} }
public async Task<ReadSessionsPlayerCountResponse?> GetSessions() public async Task<ReadSessionsPlayerCountResponse?> 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) if (response.IsSuccessStatusCode)
{ {
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions); return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
@@ -73,21 +57,20 @@ namespace Shogi.UI.Pages.Home.Api
return null; return null;
} }
public async Task<Guid?> GetToken() public async Task<CreateTokenResponse?> GetToken()
{ {
var response = await HttpClient.GetAsync(new Uri("User/Token", UriKind.Relative)); var response = await HttpClient.GetFromJsonAsync<CreateTokenResponse>(new Uri("User/Token", UriKind.Relative), serializerOptions);
var deserialized = await response.Content.ReadFromJsonAsync<CreateTokenResponse>(serializerOptions); return response;
return deserialized?.OneTimeToken;
} }
public async Task PostMove(string sessionName, Contracts.Types.Move move) 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<HttpStatusCode> PostSession(string name, bool isPrivate) public async Task<HttpStatusCode> 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, Name = name,
}); });

View File

@@ -88,7 +88,7 @@
async Task FetchSessions() async Task FetchSessions()
{ {
var sessions = await ShogiApi.GetSessions(); var sessions = await ShogiApi.GetSessionsPlayerCount();
if (sessions != null) if (sessions != null)
{ {
this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray(); this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray();