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

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

View File

@@ -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
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]
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;
public class MoveResponse : ISocketResponse
public class PlayerHasMovedMessage : ISocketResponse
{
public SocketAction Action { get; }
public string SessionName { get; set; } = string.Empty;
public string SessionName { get; set; }
/// <summary>
/// The player that made the move.
/// </summary>
public string PlayerName { get; set; } = string.Empty;
public string PlayerName { get; set; }
public MoveResponse()
public PlayerHasMovedMessage()
{
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.");
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()
{
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

View File

@@ -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
{
Task<CreateGuestTokenResponse?> GetGuestToken();
Task<Session?> GetSession(string name);
Task<ReadSessionsPlayerCountResponse?> GetSessions();
Task<Guid?> GetToken();
Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount();
Task<CreateTokenResponse?> GetToken();
Task GuestLogout();
Task PostMove(string sessionName, Move move);
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.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Shogi.UI.Pages.Home.Api
{
@@ -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<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()
{
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)
{
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<ReadSessionResponse>(serializerOptions))?.Session;
@@ -63,9 +47,9 @@ namespace Shogi.UI.Pages.Home.Api
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)
{
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
@@ -73,21 +57,20 @@ namespace Shogi.UI.Pages.Home.Api
return null;
}
public async Task<Guid?> GetToken()
public async Task<CreateTokenResponse?> GetToken()
{
var response = await HttpClient.GetAsync(new Uri("User/Token", UriKind.Relative));
var deserialized = await response.Content.ReadFromJsonAsync<CreateTokenResponse>(serializerOptions);
return deserialized?.OneTimeToken;
var response = await HttpClient.GetFromJsonAsync<CreateTokenResponse>(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<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,
});

View File

@@ -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();