This commit is contained in:
2021-08-01 17:32:43 -05:00
parent 178cb00253
commit b10f61a489
76 changed files with 1655 additions and 1185 deletions

View File

@@ -1,8 +1,10 @@
using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Linq;
using System.Threading.Tasks;
@@ -13,34 +15,36 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
[Route("[controller]")]
public class GameController : ControllerBase
{
private readonly IGameboardManager manager;
private static readonly string UsernameClaim = "preferred_username";
private readonly IGameboardManager gameboardManager;
private readonly IGameboardRepository gameboardRepository;
private readonly ISocketConnectionManager communicationManager;
private readonly IGameboardRepository repository;
private string? JwtUserName => HttpContext.User.Claims.FirstOrDefault(c => c.Type == UsernameClaim)?.Value;
public GameController(
IGameboardRepository repository,
IGameboardManager manager,
ISocketConnectionManager communicationManager)
{
this.manager = manager;
gameboardManager = manager;
gameboardRepository = repository;
this.communicationManager = communicationManager;
this.repository = repository;
}
[HttpPost("JoinCode")]
public async Task<IActionResult> PostGameInvitation([FromBody] PostGameInvitation request)
{
var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
var isPlayer1 = await manager.IsPlayer1(request.SessionName, userName);
if (isPlayer1)
{
var code = await repository.PostJoinCode(request.SessionName, userName);
return new CreatedResult("", new PostGameInvitationResponse(code));
}
else
{
return new UnauthorizedResult();
}
//var isPlayer1 = await gameboardManager.IsPlayer1(request.SessionName, userName);
//if (isPlayer1)
//{
// var code = await gameboardRepository.PostJoinCode(request.SessionName, userName);
// return new CreatedResult("", new PostGameInvitationResponse(code));
//}
//else
//{
return new UnauthorizedResult();
//}
}
[AllowAnonymous]
@@ -48,19 +52,58 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
public async Task<IActionResult> PostGuestGameInvitation([FromBody] PostGuestGameInvitation request)
{
var isGuest = manager.IsGuest(request.GuestId);
var isPlayer1 = manager.IsPlayer1(request.SessionName, request.GuestId);
if (isGuest && await isPlayer1)
{
var code = await repository.PostJoinCode(request.SessionName, request.GuestId);
return new CreatedResult("", new PostGameInvitationResponse(code));
}
else
{
return new UnauthorizedResult();
}
//var isGuest = gameboardManager.IsGuest(request.GuestId);
//var isPlayer1 = gameboardManager.IsPlayer1(request.SessionName, request.GuestId);
//if (isGuest && await isPlayer1)
//{
// var code = await gameboardRepository.PostJoinCode(request.SessionName, request.GuestId);
// return new CreatedResult("", new PostGameInvitationResponse(code));
//}
//else
//{
return new UnauthorizedResult();
//}
}
[HttpPost("{gameName}/Move")]
public async Task<IActionResult> PostMove([FromRoute] string gameName, [FromBody] PostMove request)
{
Models.User? user = null;
if (Request.Cookies.ContainsKey(SocketController.WebSessionKey))
{
var webSessionId = Guid.Parse(Request.Cookies[SocketController.WebSessionKey]!);
user = await gameboardManager.ReadUser(webSessionId);
}
else if (!string.IsNullOrEmpty(JwtUserName))
{
user = await gameboardManager.ReadUser(JwtUserName);
}
var session = await gameboardManager.ReadSession(gameName);
if (session == null || user == null || (session.Player1 != user.Name && session.Player2 != user.Name))
{
throw new UnauthorizedAccessException("You are not seated at this game.");
}
var move = request.Move;
var moveModel = move.PieceFromCaptured.HasValue
? new Models.Move(move.PieceFromCaptured.Value, move.To, move.IsPromotion)
: new Models.Move(move.From!, move.To, move.IsPromotion);
var moveSuccess = session.Shogi.Move(moveModel);
if (moveSuccess)
{
await communicationManager.BroadcastToPlayers(new MoveResponse
{
GameName = session.Name,
PlayerName = user.Name,
Move = moveModel.ToServiceModel()
}, session.Player1, session.Player2);
return Ok();
}
throw new InvalidOperationException("Illegal move.");
}
// TODO: Use JWT tokens for guests so they can authenticate and use API routes, too.
//[Route("")]
//public async Task<IActionResult> PostSession([FromBody] PostSession request)
@@ -69,7 +112,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
// var success = await repository.CreateSession(model);
// if (success)
// {
// var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Socket.Types.ClientAction.CreateGame)
// var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.ClientAction.CreateGame)
// {
// Game = model.ToServiceModel(),
// PlayerName =

View File

@@ -1,9 +1,11 @@
using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
@@ -14,10 +16,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
[ApiController]
public class SocketController : ControllerBase
{
public static readonly string WebSessionKey = "session-id";
private readonly ILogger<SocketController> logger;
private readonly ISocketTokenManager tokenManager;
private readonly IGameboardManager gameboardManager;
private readonly IGameboardRepository gameboardRepository;
private readonly CookieOptions createSessionOptions;
private readonly CookieOptions deleteSessionOptions;
public SocketController(
ILogger<SocketController> logger,
@@ -29,6 +34,23 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
this.tokenManager = tokenManager;
this.gameboardManager = gameboardManager;
this.gameboardRepository = gameboardRepository;
createSessionOptions = new CookieOptions
{
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.None,
Expires = DateTimeOffset.Now.AddYears(5)
};
deleteSessionOptions = new CookieOptions();
}
[HttpGet("Yep")]
[AllowAnonymous]
public IActionResult Yep()
{
deleteSessionOptions.Expires = DateTimeOffset.Now.AddDays(-1);
Response.Cookies.Append(WebSessionKey, "", deleteSessionOptions);
return Ok();
}
[HttpGet("Token")]
@@ -39,25 +61,36 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
return new JsonResult(new GetTokenResponse(token));
}
/// <summary>
/// Builds a token for guest users to send when requesting a socket connection.
/// Sends a HttpOnly cookie to the client with which to identify guest users.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("GuestToken")]
public async Task<IActionResult> GetGuestToken([FromQuery] GetGuestToken request)
public async Task<IActionResult> GetGuestToken()
{
if (request.ClientId == null)
var cookies = Request.Cookies;
var webSessionId = cookies.ContainsKey(WebSessionKey)
? Guid.Parse(cookies[WebSessionKey]!)
: Guid.NewGuid();
var webSessionIdAsString = webSessionId.ToString();
var user = await gameboardRepository.ReadGuestUser(webSessionId);
if (user == null)
{
var clientId = await gameboardManager.CreateGuestUser();
var token = tokenManager.GenerateToken(clientId);
return new JsonResult(new GetGuestTokenResponse(clientId, token));
var userName = await gameboardManager.CreateGuestUser(webSessionId);
var token = tokenManager.GenerateToken(webSessionIdAsString);
Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions);
return new JsonResult(new GetGuestTokenResponse(userName, token));
}
else
{
if (await gameboardRepository.IsGuestUser(request.ClientId))
{
var token = tokenManager.GenerateToken(request.ClientId);
return new JsonResult(new GetGuestTokenResponse(request.ClientId, token));
}
var token = tokenManager.GenerateToken(webSessionIdAsString);
Response.Cookies.Append(WebSessionKey, webSessionIdAsString, createSessionOptions);
return new JsonResult(new GetGuestTokenResponse(user.Name, token));
}
return new UnauthorizedResult();
}
}
}

View File

@@ -1,4 +1,4 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System;
using System.Text;
using System.Text.RegularExpressions;
@@ -26,12 +26,12 @@ namespace Gameboard.ShogiUI.Sockets.Extensions
return name;
}
public static void PrintStateAsAscii(this Models.Shogi self)
public static string PrintStateAsAscii(this Models.Shogi self)
{
var builder = new StringBuilder();
builder.Append(" Player 2(.)");
builder.AppendLine();
for (var y = 0; y < 9; y++)
for (var y = 8; y >= 0; y--)
{
builder.Append("- ");
for (var x = 0; x < 8; x++) builder.Append("- - ");
@@ -40,7 +40,7 @@ namespace Gameboard.ShogiUI.Sockets.Extensions
builder.Append('|');
for (var x = 0; x < 9; x++)
{
var piece = self.Board[y, x];
var piece = self.Board[x, y];
if (piece == null)
{
builder.Append(" ");
@@ -58,7 +58,7 @@ namespace Gameboard.ShogiUI.Sockets.Extensions
builder.Append("- -");
builder.AppendLine();
builder.Append(" Player 1");
Console.WriteLine(builder.ToString());
return builder.ToString();
}
}
}

View File

@@ -1,33 +0,0 @@
using Gameboard.ShogiUI.Sockets.Models;
using System.Collections.Concurrent;
namespace Gameboard.ShogiUI.Sockets.Managers
{
public interface IActiveSessionManager
{
void Add(Session session);
Session? Get(string sessionName);
}
// TODO: Consider moving this class' functionality into the ConnectionManager class.
public class ActiveSessionManager : IActiveSessionManager
{
private readonly ConcurrentDictionary<string, Session> Sessions;
public ActiveSessionManager()
{
Sessions = new ConcurrentDictionary<string, Session>();
}
public void Add(Session session) => Sessions.TryAdd(session.Name, session);
public Session? Get(string sessionName)
{
if (Sessions.TryGetValue(sessionName, out var session))
{
return session;
}
return null;
}
}
}

View File

@@ -1,5 +1,5 @@
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
@@ -31,14 +31,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
if (!success)
{
var error = new CreateGameResponse(request.Action)
var error = new CreateGameResponse()
{
Error = "Unable to create game with this name."
};
await connectionManager.BroadcastToPlayers(error, userName);
}
var response = new CreateGameResponse(request.Action)
var response = new CreateGameResponse()
{
PlayerName = userName,
Game = model.ToServiceModel()

View File

@@ -1,5 +1,5 @@
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers

View File

@@ -1,5 +1,5 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
@@ -24,7 +24,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{
var joinSucceeded = await gameboardManager.AssignPlayer2ToSession(request.GameName, userName);
var response = new JoinGameResponse(ClientAction.JoinGame)
var response = new JoinGameResponse()
{
PlayerName = userName,
GameName = request.GameName

View File

@@ -1,6 +1,6 @@
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.Linq;
using System.Threading.Tasks;
@@ -29,7 +29,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
var sessions = await repository.ReadSessionMetadatas();
var games = sessions.Select(s => new Game(s.Name, s.Player1, s.Player2)).ToList();
var response = new ListGamesResponse(ClientAction.ListGames)
var response = new ListGamesResponse()
{
Games = games
};

View File

@@ -1,7 +1,7 @@
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;
@@ -21,18 +21,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
private readonly ILogger<LoadGameHandler> logger;
private readonly IGameboardRepository gameboardRepository;
private readonly ISocketConnectionManager communicationManager;
private readonly IActiveSessionManager boardManager;
public LoadGameHandler(
ILogger<LoadGameHandler> logger,
ISocketConnectionManager communicationManager,
IGameboardRepository gameboardRepository,
IActiveSessionManager boardManager)
IGameboardRepository gameboardRepository)
{
this.logger = logger;
this.gameboardRepository = gameboardRepository;
this.communicationManager = communicationManager;
this.boardManager = boardManager;
}
public async Task Handle(LoadGameRequest request, string userName)
@@ -41,15 +38,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
if (sessionModel == null)
{
logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName);
var error = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." };
var error = new LoadGameResponse() { Error = "Game not found." };
await communicationManager.BroadcastToPlayers(error, userName);
return;
}
communicationManager.SubscribeToGame(sessionModel, userName);
boardManager.Add(sessionModel);
var response = new LoadGameResponse(ClientAction.LoadGame)
var response = new LoadGameResponse()
{
Game = new SessionMetadata(sessionModel).ToServiceModel(),
BoardState = sessionModel.Shogi.ToServiceModel(),

View File

@@ -1,9 +1,6 @@
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{
public interface IMoveHandler
@@ -12,66 +9,53 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
}
public class MoveHandler : IMoveHandler
{
private readonly IActiveSessionManager boardManager;
private readonly IGameboardManager gameboardManager;
private readonly ISocketConnectionManager communicationManager;
private readonly ISocketConnectionManager connectionManager;
public MoveHandler(
IActiveSessionManager boardManager,
ISocketConnectionManager communicationManager,
ISocketConnectionManager connectionManager,
IGameboardManager gameboardManager)
{
this.boardManager = boardManager;
this.gameboardManager = gameboardManager;
this.communicationManager = communicationManager;
this.connectionManager = connectionManager;
}
public async Task Handle(MoveRequest request, string userName)
{
Move moveModel;
Models.Move moveModel;
if (request.Move.PieceFromCaptured.HasValue)
{
moveModel = new Move(request.Move.PieceFromCaptured.Value, request.Move.To);
moveModel = new Models.Move(request.Move.PieceFromCaptured.Value, request.Move.To);
}
else
{
moveModel = new Move(request.Move.From!, request.Move.To, request.Move.IsPromotion);
moveModel = new Models.Move(request.Move.From!, request.Move.To, request.Move.IsPromotion);
}
var board = boardManager.Get(request.GameName);
if (board == null)
var session = await gameboardManager.ReadSession(request.GameName);
if (session != null)
{
// TODO: Find a flow for this
var response = new MoveResponse(ServiceModels.Socket.Types.ClientAction.Move)
var shogi = session.Shogi;
var moveSuccess = shogi.Move(moveModel);
if (moveSuccess)
{
Error = $"Game isn't loaded. Send a message with the {ServiceModels.Socket.Types.ClientAction.LoadGame} action first."
};
await communicationManager.BroadcastToPlayers(response, userName);
await gameboardManager.CreateBoardState(session.Name, shogi);
var response = new MoveResponse()
{
GameName = request.GameName,
PlayerName = userName,
Move = moveModel.ToServiceModel()
};
await connectionManager.BroadcastToPlayers(response, session.Player1, session.Player2);
}
else
{
var response = new MoveResponse()
{
Error = "Invalid move."
};
await connectionManager.BroadcastToPlayers(response, userName);
}
}
//var moveSuccess = board.Move(boardMove);
//if (moveSuccess)
//{
// await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel()));
// var boardState = new BoardState(board);
// var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move)
// {
// GameName = request.GameName,
// PlayerName = userName,
// BoardState = boardState.ToServiceModel()
// };
// await communicationManager.BroadcastToGame(request.GameName, response);
//}
//else
//{
// var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move)
// {
// Error = "Invalid move."
// };
// await communicationManager.BroadcastToPlayers(response, userName);
//}
}
}
}

View File

@@ -7,19 +7,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers
{
public interface IGameboardManager
{
Task<string> CreateGuestUser();
Task<string> CreateGuestUser(Guid webSessionId);
Task<bool> IsPlayer1(string sessionName, string playerName);
bool IsGuest(string playerName);
Task<bool> CreateSession(SessionMetadata session);
Task<Session?> ReadSession(string gameName);
Task<bool> UpdateSession(Session session);
Task<bool> UpdateSession(SessionMetadata session);
Task<bool> AssignPlayer2ToSession(string sessionName, string userName);
Task<bool> CreateBoardState(string sessionName, Shogi shogi);
Task<User?> ReadUser(string userName);
Task<User?> ReadUser(Guid webSessionId);
}
public class GameboardManager : IGameboardManager
{
private const int MaxTries = 3;
private const string GuestPrefix = "Guest-";
private readonly IGameboardRepository repository;
public GameboardManager(IGameboardRepository repository)
@@ -27,22 +28,30 @@ namespace Gameboard.ShogiUI.Sockets.Managers
this.repository = repository;
}
public async Task<string> CreateGuestUser()
public async Task<string> CreateGuestUser(Guid webSessionId)
{
var count = 0;
while (count < MaxTries)
{
count++;
var clientId = $"Guest-{Guid.NewGuid()}";
var isCreated = await repository.CreateGuestUser(clientId);
var isCreated = await repository.CreateGuestUser(clientId, webSessionId);
if (isCreated)
{
return clientId;
}
}
throw new OperationCanceledException($"Failed to create guest user after {MaxTries} tries.");
throw new OperationCanceledException($"Failed to create guest user after {count} tries.");
}
public Task<User?> ReadUser(Guid webSessionId)
{
return repository.ReadGuestUser(webSessionId);
}
public Task<User?> ReadUser(string userName)
{
return repository.ReadUser(userName);
}
public async Task<bool> IsPlayer1(string sessionName, string playerName)
{
//var session = await repository.GetGame(sessionName);
@@ -65,8 +74,6 @@ namespace Gameboard.ShogiUI.Sockets.Managers
return repository.CreateSession(session);
}
public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix);
public Task<Session?> ReadSession(string sessionName)
{
return repository.ReadSession(sessionName);
@@ -77,15 +84,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers
/// </summary>
/// <param name="session">The session to save.</param>
/// <returns>True if the session was saved successfully.</returns>
public Task<bool> UpdateSession(Session session)
public Task<bool> UpdateSession(SessionMetadata session)
{
return repository.UpdateSession(session);
}
public Task<bool> CreateBoardState(string sessionName, Shogi shogi)
{
return repository.CreateBoardState(sessionName, shogi);
}
public async Task<bool> AssignPlayer2ToSession(string sessionName, string userName)
{
var isSuccess = false;
var session = await repository.ReadSession(sessionName);
var session = await repository.ReadSessionMetaData(sessionName);
if (session != null && !session.IsPrivate && string.IsNullOrEmpty(session.Player2))
{
session.SetPlayer2(userName);

View File

@@ -1,6 +1,6 @@
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Collections.Concurrent;
@@ -19,7 +19,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
void SubscribeToBroadcast(WebSocket socket, string playerName);
void UnsubscribeFromBroadcastAndGames(string playerName);
void UnsubscribeFromGame(string gameName, string playerName);
Task BroadcastToPlayers(IResponse response, params string[] playerNames);
Task BroadcastToPlayers(IResponse response, params string?[] playerNames);
}
/// <summary>
@@ -84,17 +84,16 @@ namespace Gameboard.ShogiUI.Sockets.Managers
}
}
public async Task BroadcastToPlayers(IResponse response, params string[] playerNames)
public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames)
{
var tasks = new List<Task>(playerNames.Length);
foreach (var name in playerNames)
{
if (connections.TryGetValue(name, out var socket))
if (!string.IsNullOrEmpty(name) && connections.TryGetValue(name, out var socket))
{
var serialized = JsonConvert.SerializeObject(response);
logger.LogInformation("Response to {0} \n{1}\n", name, serialized);
tasks.Add(socket.SendTextAsync(serialized));
}
}
await Task.WhenAll(tasks);

View File

@@ -15,7 +15,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
public class SocketTokenManager : ISocketTokenManager
{
/// <summary>
/// Key is userName
/// Key is userName or webSessionId
/// </summary>
private readonly ConcurrentDictionary<string, Guid> Tokens;

View File

@@ -1,10 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Managers.Utility
{
public class Request : IRequest
{
public ClientAction Action { get; set; }
}
}

View File

@@ -1,18 +1,14 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using System;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Gameboard.ShogiUI.Sockets.Utilities;
using System.Diagnostics;
using System.Numerics;
using System.Text.RegularExpressions;
namespace Gameboard.ShogiUI.Sockets.Models
{
[DebuggerDisplay("{From} - {To}")]
public class Move
{
private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
private static readonly char A = 'A';
public Vector2? From { get; }
public Vector2? From { get; } // TODO: Use string notation
public bool IsPromotion { get; }
public WhichPiece? PieceFromHand { get; }
public Vector2 To { get; }
@@ -37,8 +33,8 @@ namespace Gameboard.ShogiUI.Sockets.Models
/// <param name="isPromotion">If the moving piece should be promoted.</param>
public Move(string fromNotation, string toNotation, bool isPromotion = false)
{
From = FromBoardNotation(fromNotation);
To = FromBoardNotation(toNotation);
From = NotationHelper.FromBoardNotation(fromNotation);
To = NotationHelper.FromBoardNotation(toNotation);
IsPromotion = isPromotion;
}
@@ -52,35 +48,16 @@ namespace Gameboard.ShogiUI.Sockets.Models
{
From = null;
PieceFromHand = pieceFromHand;
To = FromBoardNotation(toNotation);
To = NotationHelper.FromBoardNotation(toNotation);
IsPromotion = isPromotion;
}
public ServiceModels.Socket.Types.Move ToServiceModel() => new()
public ServiceModels.Types.Move ToServiceModel() => new()
{
From = From.HasValue ? ToBoardNotation(From.Value) : null,
From = From.HasValue ? NotationHelper.ToBoardNotation(From.Value) : null,
IsPromotion = IsPromotion,
PieceFromCaptured = PieceFromHand.HasValue ? PieceFromHand : null,
To = ToBoardNotation(To)
To = NotationHelper.ToBoardNotation(To)
};
private static string ToBoardNotation(Vector2 vector)
{
var file = (char)(vector.X + A);
var rank = vector.Y + 1;
return $"{file}{rank}";
}
private static Vector2 FromBoardNotation(string notation)
{
if (Regex.IsMatch(notation, BoardNotationRegex))
{
var match = Regex.Match(notation, BoardNotationRegex);
char file = match.Groups["file"].Value[0];
int rank = int.Parse(match.Groups["rank"].Value);
return new Vector2(file - A, rank);
}
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
}
}
}

View File

@@ -1,4 +1,4 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using PathFinding;
using System.Diagnostics;
@@ -18,6 +18,9 @@ namespace Gameboard.ShogiUI.Sockets.Models
Owner = owner;
IsPromoted = isPromoted;
}
public Piece(Piece piece) : this(piece.WhichPiece, piece.Owner, piece.IsPromoted)
{
}
public bool CanPromote => !IsPromoted
&& WhichPiece != WhichPiece.King
@@ -54,9 +57,9 @@ namespace Gameboard.ShogiUI.Sockets.Models
_ => throw new System.NotImplementedException()
};
public ServiceModels.Socket.Types.Piece ToServiceModel()
public ServiceModels.Types.Piece ToServiceModel()
{
return new ServiceModels.Socket.Types.Piece
return new ServiceModels.Types.Piece
{
IsPromoted = IsPromoted,
Owner = Owner,

View File

@@ -7,7 +7,7 @@
{
public string Name { get; }
public string Player1 { get; }
public string? Player2 { get; }
public string? Player2 { get; private set; }
public bool IsPrivate { get; }
public SessionMetadata(string name, bool isPrivate, string player1, string? player2)
@@ -25,6 +25,11 @@
Player2 = sessionModel.Player2;
}
public ServiceModels.Socket.Types.Game ToServiceModel() => new(Name, Player1, Player2);
public void SetPlayer2(string playerName)
{
Player2 = playerName;
}
public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1, Player2);
}
}

View File

@@ -1,4 +1,5 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Gameboard.ShogiUI.Sockets.Utilities;
using PathFinding;
using System;
using System.Collections.Generic;
@@ -19,8 +20,10 @@ namespace Gameboard.ShogiUI.Sockets.Models
private Shogi? validationBoard;
private Vector2 player1King;
private Vector2 player2King;
public IReadOnlyDictionary<WhichPlayer, List<Piece>> Hands { get; }
public PlanarCollection<Piece> Board { get; } //TODO: Hide this being a getter method
private List<Piece> Hand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
public List<Piece> Player1Hand { get; }
public List<Piece> Player2Hand { get; }
public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method
public List<Move> MoveHistory { get; }
public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2;
public WhichPlayer? InCheck { get; private set; }
@@ -30,15 +33,13 @@ namespace Gameboard.ShogiUI.Sockets.Models
public Shogi()
{
Board = new PlanarCollection<Piece>(9, 9);
Board = new CoordsToNotationCollection();
MoveHistory = new List<Move>(20);
Hands = new Dictionary<WhichPlayer, List<Piece>> {
{ WhichPlayer.Player1, new List<Piece>()},
{ WhichPlayer.Player2, new List<Piece>()},
};
pathFinder = new PathFinder2D<Piece>(Board);
player1King = new Vector2(4, 8);
player2King = new Vector2(4, 0);
Player1Hand = new List<Piece>();
Player2Hand = new List<Piece>();
pathFinder = new PathFinder2D<Piece>(Board, 9, 9);
player1King = new Vector2(4, 0);
player2King = new Vector2(4, 8);
Error = string.Empty;
InitializeBoardState();
@@ -58,24 +59,16 @@ namespace Gameboard.ShogiUI.Sockets.Models
private Shogi(Shogi toCopy)
{
Board = new PlanarCollection<Piece>(9, 9);
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var piece = toCopy.Board[y, x];
if (piece != null)
{
Board[y, x] = new Piece(piece.WhichPiece, piece.Owner, piece.IsPromoted);
}
}
pathFinder = new PathFinder2D<Piece>(Board);
MoveHistory = new List<Move>(toCopy.MoveHistory);
Hands = new Dictionary<WhichPlayer, List<Piece>>
Board = new CoordsToNotationCollection();
foreach (var kvp in toCopy.Board)
{
{ WhichPlayer.Player1, new List<Piece>(toCopy.Hands[WhichPlayer.Player1]) },
{ WhichPlayer.Player2, new List<Piece>(toCopy.Hands[WhichPlayer.Player2]) }
};
Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value);
}
pathFinder = new PathFinder2D<Piece>(Board, 9, 9);
MoveHistory = new List<Move>(toCopy.MoveHistory);
Player1Hand = new List<Piece>(toCopy.Player1Hand);
Player2Hand = new List<Piece>(toCopy.Player2Hand);
player1King = toCopy.player1King;
player2King = toCopy.player2King;
Error = toCopy.Error;
@@ -115,6 +108,8 @@ namespace Gameboard.ShogiUI.Sockets.Models
: validationBoard.PlaceFromBoard(move);
if (!isValid)
{
// Surface the error description.
Error = validationBoard.Error;
// Invalidate the "throw away" board.
validationBoard = null;
return false;
@@ -137,37 +132,55 @@ namespace Gameboard.ShogiUI.Sockets.Models
/// <returns>True if the move was successful.</returns>
private bool PlaceFromHand(Move move)
{
if (move.PieceFromHand.HasValue == false) return false; //Invalid move
var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand);
if (index < 0) return false; // Invalid move
if (Board[move.To.Y, move.To.X] != null) return false; // Invalid move; cannot capture while playing from the hand.
var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand);
if (index < 0)
{
Error = $"{move.PieceFromHand} does not exist in the hand.";
return false;
}
if (Board[move.To] != null)
{
Error = $"Illegal move - attempting to capture while playing a piece from the hand.";
return false;
}
var minimumY = 0;
switch (move.PieceFromHand.Value)
switch (move.PieceFromHand!.Value)
{
case WhichPiece.Knight:
// Knight cannot be placed onto the farthest two ranks from the hand.
minimumY = WhoseTurn == WhichPlayer.Player1 ? 6 : 2;
break;
{
// Knight cannot be placed onto the farthest two ranks from the hand.
if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y > 6)
|| (WhoseTurn == WhichPlayer.Player2 && move.To.Y < 2))
{
Error = $"Knight has no valid moves after placed.";
return false;
}
break;
}
case WhichPiece.Lance:
case WhichPiece.Pawn:
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
minimumY = WhoseTurn == WhichPlayer.Player1 ? 7 : 1;
break;
{
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
if ((WhoseTurn == WhichPlayer.Player1 && move.To.Y == 8)
|| (WhoseTurn == WhichPlayer.Player2 && move.To.Y == 0))
{
Error = $"{move.PieceFromHand} has no valid moves after placed.";
return false;
}
break;
}
}
if (WhoseTurn == WhichPlayer.Player1 && move.To.Y < minimumY) return false;
if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false;
// Mutate the board.
Board[move.To.Y, move.To.X] = Hands[WhoseTurn][index];
Hands[WhoseTurn].RemoveAt(index);
Board[move.To] = Hand[index];
Hand.RemoveAt(index);
return true;
}
/// <returns>True if the move was successful.</returns>
private bool PlaceFromBoard(Move move)
{
var fromPiece = Board[move.From.Value.Y, move.From.Value.X];
var fromPiece = Board[move.From!.Value];
if (fromPiece == null)
{
Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}.";
@@ -184,28 +197,28 @@ namespace Gameboard.ShogiUI.Sockets.Models
return false; // Invalid move; move not part of move-set.
}
var captured = Board[move.To.Y, move.To.X];
var captured = Board[move.To];
if (captured != null)
{
if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece.
captured.Capture();
Hands[captured.Owner].Add(captured);
Hand.Add(captured);
}
//Mutate the board.
if (move.IsPromotion)
{
if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Value.Y < 3))
if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5))
{
fromPiece.Promote();
}
else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Value.Y > 5))
else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3))
{
fromPiece.Promote();
}
}
Board[move.To.Y, move.To.X] = fromPiece;
Board[move.From.Value.Y, move.From.Value.X] = null;
Board[move.To] = fromPiece;
Board[move.From!.Value] = null;
if (fromPiece.WhichPiece == WhichPiece.King)
{
if (fromPiece.Owner == WhichPlayer.Player1)
@@ -225,7 +238,7 @@ namespace Gameboard.ShogiUI.Sockets.Models
private bool IsPathable(Vector2 from, Vector2 to)
{
var piece = Board[from.Y, from.X];
var piece = Board[from];
if (piece == null) return false;
var isObstructed = false;
@@ -239,56 +252,66 @@ namespace Gameboard.ShogiUI.Sockets.Models
#region Rules Validation
private bool EvaluateCheckAfterMove(Move move, WhichPlayer whichPlayer)
{
if (whichPlayer == InCheck) return true; // If we already know the player is in check, don't bother.
var isCheck = false;
var kingPosition = whichPlayer == WhichPlayer.Player1 ? player1King : player2King;
// Check if the move put the king in check.
if (pathFinder.PathTo(move.To, kingPosition)) return true;
// Get line equation from king through the now-unoccupied location.
var direction = Vector2.Subtract(kingPosition, move.From.Value);
var slope = Math.Abs(direction.Y / direction.X);
// If absolute slope is 45°, look for a bishop along the line.
// If absolute slope is 0° or 90°, look for a rook along the line.
// if absolute slope is 0°, look for lance along the line.
if (float.IsInfinity(slope))
if (move.From.HasValue)
{
// if slope of the move is also infinity...can skip this?
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
// Get line equation from king through the now-unoccupied location.
var direction = Vector2.Subtract(kingPosition, move.From!.Value);
var slope = Math.Abs(direction.Y / direction.X);
// If absolute slope is 45°, look for a bishop along the line.
// If absolute slope is 0° or 90°, look for a rook along the line.
// if absolute slope is 0°, look for lance along the line.
if (float.IsInfinity(slope))
{
if (piece.Owner != whichPlayer)
// if slope of the move is also infinity...can skip this?
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
{
switch (piece.WhichPiece)
if (piece.Owner != whichPlayer)
{
case WhichPiece.Rook:
isCheck = true;
break;
case WhichPiece.Lance:
if (!piece.IsPromoted) isCheck = true;
break;
switch (piece.WhichPiece)
{
case WhichPiece.Rook:
isCheck = true;
break;
case WhichPiece.Lance:
if (!piece.IsPromoted) isCheck = true;
break;
}
}
}
});
}
else if (slope == 1)
{
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
});
}
else if (slope == 1)
{
if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop)
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
{
isCheck = true;
}
});
}
else if (slope == 0)
{
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Bishop)
{
isCheck = true;
}
});
}
else if (slope == 0)
{
if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook)
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
{
isCheck = true;
}
});
if (piece.Owner != whichPlayer && piece.WhichPiece == WhichPiece.Rook)
{
isCheck = true;
}
});
}
}
else
{
// TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent.
// Go read the shogi rules to be sure this is true.
}
return isCheck;
@@ -299,12 +322,11 @@ namespace Gameboard.ShogiUI.Sockets.Models
// Assume true and try to disprove.
var isCheckmate = true;
Board.ForEachNotNull((piece, x, y) => // For each piece...
Board.ForEachNotNull((piece, from) => // For each piece...
{
// Short circuit
if (!isCheckmate) return;
var from = new Vector2(x, y);
if (piece.Owner == InCheck) // ...owned by the player in check...
{
// ...evaluate if any move gets the player out of check.
@@ -328,82 +350,108 @@ namespace Gameboard.ShogiUI.Sockets.Models
}
#endregion
#region Initialize
private void ResetEmptyRows()
{
for (int y = 3; y < 6; y++)
for (int x = 0; x < 9; x++)
Board[y, x] = null;
}
private void ResetFrontRow(WhichPlayer player)
{
int y = player == WhichPlayer.Player1 ? 6 : 2;
for (int x = 0; x < 9; x++) Board[y, x] = new Piece(WhichPiece.Pawn, player);
}
private void ResetMiddleRow(WhichPlayer player)
{
int y = player == WhichPlayer.Player1 ? 7 : 1;
Board[y, 0] = null;
for (int x = 2; x < 7; x++) Board[y, x] = null;
Board[y, 8] = null;
if (player == WhichPlayer.Player1)
{
Board[y, 1] = new Piece(WhichPiece.Bishop, player);
Board[y, 7] = new Piece(WhichPiece.Rook, player);
}
else
{
Board[y, 1] = new Piece(WhichPiece.Rook, player);
Board[y, 7] = new Piece(WhichPiece.Bishop, player);
}
}
private void ResetRearRow(WhichPlayer player)
{
int y = player == WhichPlayer.Player1 ? 8 : 0;
Board[y, 0] = new Piece(WhichPiece.Lance, player);
Board[y, 1] = new Piece(WhichPiece.Knight, player);
Board[y, 2] = new Piece(WhichPiece.SilverGeneral, player);
Board[y, 3] = new Piece(WhichPiece.GoldGeneral, player);
Board[y, 4] = new Piece(WhichPiece.King, player);
Board[y, 5] = new Piece(WhichPiece.GoldGeneral, player);
Board[y, 6] = new Piece(WhichPiece.SilverGeneral, player);
Board[y, 7] = new Piece(WhichPiece.Knight, player);
Board[y, 8] = new Piece(WhichPiece.Lance, player);
}
private void InitializeBoardState()
{
ResetRearRow(WhichPlayer.Player2);
ResetMiddleRow(WhichPlayer.Player2);
ResetFrontRow(WhichPlayer.Player2);
ResetEmptyRows();
ResetFrontRow(WhichPlayer.Player1);
ResetMiddleRow(WhichPlayer.Player1);
ResetRearRow(WhichPlayer.Player1);
Board["A1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1);
Board["B1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1);
Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1);
Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1);
Board["E1"] = new Piece(WhichPiece.King, WhichPlayer.Player1);
Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player1);
Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player1);
Board["H1"] = new Piece(WhichPiece.Knight, WhichPlayer.Player1);
Board["I1"] = new Piece(WhichPiece.Lance, WhichPlayer.Player1);
Board["A2"] = null;
Board["B2"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player1);
Board["C2"] = null;
Board["D2"] = null;
Board["E2"] = null;
Board["F2"] = null;
Board["G2"] = null;
Board["H2"] = new Piece(WhichPiece.Rook, WhichPlayer.Player1);
Board["I2"] = null;
Board["A3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["B3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["C3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["D3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["E3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["F3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["G3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["H3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["I3"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player1);
Board["A4"] = null;
Board["B4"] = null;
Board["C4"] = null;
Board["D4"] = null;
Board["E4"] = null;
Board["F4"] = null;
Board["G4"] = null;
Board["H4"] = null;
Board["I4"] = null;
Board["A5"] = null;
Board["B5"] = null;
Board["C5"] = null;
Board["D5"] = null;
Board["E5"] = null;
Board["F5"] = null;
Board["G5"] = null;
Board["H5"] = null;
Board["I5"] = null;
Board["A6"] = null;
Board["B6"] = null;
Board["C6"] = null;
Board["D6"] = null;
Board["E6"] = null;
Board["F6"] = null;
Board["G6"] = null;
Board["H6"] = null;
Board["I6"] = null;
Board["A7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["B7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["C7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["D7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["E7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["F7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["G7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["H7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["I7"] = new Piece(WhichPiece.Pawn, WhichPlayer.Player2);
Board["A8"] = null;
Board["B8"] = new Piece(WhichPiece.Rook, WhichPlayer.Player2);
Board["C8"] = null;
Board["D8"] = null;
Board["E8"] = null;
Board["F8"] = null;
Board["G8"] = null;
Board["H8"] = new Piece(WhichPiece.Bishop, WhichPlayer.Player2);
Board["I8"] = null;
Board["A9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2);
Board["B9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2);
Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2);
Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2);
Board["E9"] = new Piece(WhichPiece.King, WhichPlayer.Player2);
Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPlayer.Player2);
Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPlayer.Player2);
Board["H9"] = new Piece(WhichPiece.Knight, WhichPlayer.Player2);
Board["I9"] = new Piece(WhichPiece.Lance, WhichPlayer.Player2);
}
#endregion
public BoardState ToServiceModel()
{
var board = new ServiceModels.Socket.Types.Piece[9, 9];
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var piece = Board[y, x];
if (piece != null)
{
board[y, x] = piece.ToServiceModel();
}
}
return new BoardState
{
Board = board,
Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()),
PlayerInCheck = InCheck,
WhoseTurn = WhoseTurn,
Player1Hand = Hands[WhichPlayer.Player1].Select(_ => _.ToServiceModel()).ToList(),
Player2Hand = Hands[WhichPlayer.Player2].Select(_ => _.ToServiceModel()).ToList()
Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(),
Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList()
};
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace Gameboard.ShogiUI.Sockets.Models
{
public class User
{
public static readonly string GuestPrefix = "Guest-";
public string Name { get; }
public Guid? WebSessionId { get; }
public bool IsGuest => Name.StartsWith(GuestPrefix) && WebSessionId.HasValue;
public User(string name, Guid? webSessionId = null)
{
Name = name;
WebSessionId = webSessionId;
}
}
}

View File

@@ -1,6 +1,9 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Gameboard.ShogiUI.Sockets.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
@@ -8,7 +11,10 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public string Name { get; set; }
public Piece?[,] Board { get; set; }
/// <summary>
/// A dictionary where the key is a board-notation position, like D3.
/// </summary>
public Dictionary<string, Piece?> Board { get; set; }
public Piece[] Player1Hand { get; set; }
@@ -25,7 +31,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
public BoardStateDocument() : base(WhichDocumentType.BoardState)
{
Name = string.Empty;
Board = new Piece[9, 9];
Board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
Player1Hand = Array.Empty<Piece>();
Player2Hand = Array.Empty<Piece>();
}
@@ -34,20 +40,23 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
: base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState)
{
Name = sessionName;
Board = new Piece[9, 9];
Board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var position = new Vector2(x, y);
var piece = shogi.Board[y, x];
if (piece != null)
{
Board[y, x] = new Piece(piece);
var positionNotation = NotationHelper.ToBoardNotation(position);
Board[positionNotation] = new Piece(piece);
}
}
Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(model => new Piece(model)).ToArray();
Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(model => new Piece(model)).ToArray();
Player1Hand = shogi.Player1Hand.Select(model => new Piece(model)).ToArray();
Player2Hand = shogi.Player2Hand.Select(model => new Piece(model)).ToArray();
if (shogi.MoveHistory.Count > 0)
{
Move = new Move(shogi.MoveHistory[^1]);

View File

@@ -5,10 +5,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
internal class CouchFindResult<T>
{
public T[] docs;
public string warning;
public CouchFindResult()
{
docs = Array.Empty<T>();
warning = "";
}
}
}

View File

@@ -1,4 +1,4 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.Numerics;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels

View File

@@ -1,4 +1,4 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{

View File

@@ -1,9 +1,9 @@
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
using System;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class UserDocument : CouchDocument
{
public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}";
public enum LoginPlatform
{
Microsoft,
@@ -12,10 +12,27 @@
public string Name { get; set; }
public LoginPlatform Platform { get; set; }
public UserDocument(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", WhichDocumentType.User)
/// <summary>
/// The browser session ID saved via Set-Cookie headers.
/// Only used with guest accounts.
/// </summary>
public Guid? WebSessionId { get; set; }
/// <summary>
/// Constructor for JSON deserializing.
/// </summary>
public UserDocument() : base(WhichDocumentType.User)
{
Name = string.Empty;
}
public UserDocument(string name, Guid? webSessionId = null) : base($"org.couchdb.user:{name}", WhichDocumentType.User)
{
Name = name;
Platform = platform;
WebSessionId = webSessionId;
Platform = WebSessionId.HasValue
? LoginPlatform.Guest
: LoginPlatform.Microsoft;
}
}
}

View File

@@ -12,14 +12,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
{
public interface IGameboardRepository
{
Task<bool> CreateGuestUser(string userName);
Task<bool> CreateBoardState(string sessionName, Models.Shogi shogi);
Task<bool> CreateSession(Models.SessionMetadata session);
Task<bool> CreateUser(Models.User user);
Task<IList<Models.SessionMetadata>> ReadSessionMetadatas();
Task<bool> IsGuestUser(string userName);
Task<string> PostJoinCode(string gameName, string userName);
Task<Models.User?> ReadGuestUser(Guid webSessionId);
Task<Models.Session?> ReadSession(string name);
Task<Models.Shogi?> ReadShogi(string name);
Task<bool> UpdateSession(Models.Session session);
Task<bool> UpdateSession(Models.SessionMetadata session);
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
Task<Models.User?> ReadUser(string userName);
}
public class GameboardRepository : IGameboardRepository
@@ -65,6 +67,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return couchModel.ToDomainModel(shogi);
}
public async Task<Models.SessionMetadata?> ReadSessionMetaData(string name)
{
var response = await client.GetAsync(name);
var responseContent = await response.Content.ReadAsStringAsync();
var couchModel = JsonConvert.DeserializeObject<SessionDocument>(responseContent);
return couchModel.ToDomainModel();
}
public async Task<Models.Shogi?> ReadShogi(string name)
{
var selector = new Dictionary<string, object>(2)
@@ -74,7 +84,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
};
var sort = new Dictionary<string, object>(1)
{
[nameof(BoardStateDocument.CreatedDate)] = "desc"
[nameof(BoardStateDocument.CreatedDate)] = "asc"
};
var query = JsonConvert.SerializeObject(new { selector, sort = new[] { sort } });
@@ -103,6 +113,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return new Models.Shogi(moves);
}
public async Task<bool> CreateBoardState(string sessionName, Models.Shogi shogi)
{
var boardStateDocument = new BoardStateDocument(sessionName, shogi);
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
}
public async Task<bool> CreateSession(Models.SessionMetadata session)
{
var sessionDocument = new SessionDocument(session);
@@ -120,7 +138,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return false;
}
public async Task<bool> UpdateSession(Models.Session session)
public async Task<bool> UpdateSession(Models.SessionMetadata session)
{
var couchModel = new SessionDocument(session);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
@@ -178,32 +196,53 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return string.Empty;
}
//public async Task<Player> GetPlayer(string playerName)
//{
// var uri = $"Player/{playerName}";
// var get = await client.GetAsync(Uri.EscapeUriString(uri));
// var content = await get.Content.ReadAsStringAsync();
// if (!string.IsNullOrWhiteSpace(content))
// {
// var response = JsonConvert.DeserializeObject<GetPlayerResponse>(content);
// return new Player(response.Player.Name);
// }
// return null;
//}
public async Task<bool> CreateGuestUser(string userName)
public async Task<Models.User?> ReadUser(string userName)
{
var couchModel = new UserDocument(userName, UserDocument.LoginPlatform.Guest);
var document = new UserDocument(userName);
var response = await client.GetAsync(document.Id);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var user = JsonConvert.DeserializeObject<UserDocument>(responseContent);
return new Models.User(user.Name);
}
return null;
}
public async Task<bool> CreateUser(Models.User user)
{
var couchModel = new UserDocument(user.Name, user.WebSessionId);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
}
public async Task<bool> IsGuestUser(string userName)
public async Task<Models.User?> ReadGuestUser(Guid webSessionId)
{
var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{UserDocument.GetDocumentId(userName)}"));
var response = await client.SendAsync(req);
return response.IsSuccessStatusCode;
var selector = new Dictionary<string, object>(2)
{
[nameof(UserDocument.DocumentType)] = WhichDocumentType.User,
[nameof(UserDocument.WebSessionId)] = webSessionId.ToString()
};
var query = JsonConvert.SerializeObject(new { selector, limit = 1 });
var content = new StringContent(query, Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadGuestUser), responseContent, query);
return null;
}
var result = JsonConvert.DeserializeObject<CouchFindResult<UserDocument>>(responseContent);
if (!string.IsNullOrWhiteSpace(result.warning))
{
logger.LogError(new InvalidOperationException(result.warning), result.warning);
return null;
}
return new Models.User(result.docs.Single().Name);
}
}
}

View File

@@ -1,6 +1,6 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{

View File

@@ -1,6 +1,6 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{

View File

@@ -1,6 +1,6 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{

View File

@@ -1,6 +1,6 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{

View File

@@ -1,6 +1,6 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{

View File

@@ -1,6 +1,6 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{

View File

@@ -1,11 +1,12 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.Controllers;
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
using Gameboard.ShogiUI.Sockets.Managers.Utility;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Gameboard.ShogiUI.Sockets.Services.Utility;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -29,6 +30,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
{
private readonly ILogger<SocketService> logger;
private readonly ISocketConnectionManager communicationManager;
private readonly IGameboardRepository gameboardRepository;
private readonly ISocketTokenManager tokenManager;
private readonly ICreateGameHandler createGameHandler;
private readonly IJoinByCodeHandler joinByCodeHandler;
@@ -46,6 +48,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
public SocketService(
ILogger<SocketService> logger,
ISocketConnectionManager communicationManager,
IGameboardRepository gameboardRepository,
ISocketTokenManager tokenManager,
ICreateGameHandler createGameHandler,
IJoinByCodeHandler joinByCodeHandler,
@@ -63,6 +66,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
{
this.logger = logger;
this.communicationManager = communicationManager;
this.gameboardRepository = gameboardRepository;
this.tokenManager = tokenManager;
this.createGameHandler = createGameHandler;
this.joinByCodeHandler = joinByCodeHandler;
@@ -80,105 +84,115 @@ namespace Gameboard.ShogiUI.Sockets.Services
public async Task HandleSocketRequest(HttpContext context)
{
var hasToken = context.Request.Query.Keys.Contains("token");
if (hasToken)
string? userName = null;
if (context.Request.Cookies.ContainsKey(SocketController.WebSessionKey))
{
var oneTimeToken = context.Request.Query["token"][0];
var tokenAsGuid = Guid.Parse(oneTimeToken);
var userName = tokenManager.GetUsername(tokenAsGuid);
if (userName != null)
{
var socket = await context.WebSockets.AcceptWebSocketAsync();
// Guest account
var webSessionId = Guid.Parse(context.Request.Cookies[SocketController.WebSessionKey]!);
userName = (await gameboardRepository.ReadGuestUser(webSessionId))?.Name;
}
else if (context.Request.Query.Keys.Contains("token"))
{
// Microsoft account
var token = Guid.Parse(context.Request.Query["token"][0]);
userName = tokenManager.GetUsername(token);
}
communicationManager.SubscribeToBroadcast(socket, userName);
while (socket.State == WebSocketState.Open)
if (string.IsNullOrEmpty(userName))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
else
{
var socket = await context.WebSockets.AcceptWebSocketAsync();
communicationManager.SubscribeToBroadcast(socket, userName);
while (socket.State == WebSocketState.Open)
{
try
{
try
var message = await socket.ReceiveTextAsync();
if (string.IsNullOrWhiteSpace(message)) continue;
logger.LogInformation("Request \n{0}\n", message);
var request = JsonConvert.DeserializeObject<Request>(message);
if (!Enum.IsDefined(typeof(ClientAction), request.Action))
{
var message = await socket.ReceiveTextAsync();
if (string.IsNullOrWhiteSpace(message)) continue;
logger.LogInformation("Request \n{0}\n", message);
var request = JsonConvert.DeserializeObject<Request>(message);
if (!Enum.IsDefined(typeof(ClientAction), request.Action))
{
await socket.SendTextAsync("Error: Action not recognized.");
continue;
}
switch (request.Action)
{
case ClientAction.ListGames:
{
var req = JsonConvert.DeserializeObject<ListGamesRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req))
{
await listGamesHandler.Handle(req, userName);
}
break;
}
case ClientAction.CreateGame:
{
var req = JsonConvert.DeserializeObject<CreateGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req))
{
await createGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.JoinGame:
{
var req = JsonConvert.DeserializeObject<JoinGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req))
{
await joinGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.JoinByCode:
{
var req = JsonConvert.DeserializeObject<JoinByCodeRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req))
{
await joinByCodeHandler.Handle(req, userName);
}
break;
}
case ClientAction.LoadGame:
{
var req = JsonConvert.DeserializeObject<LoadGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req))
{
await loadGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.Move:
{
var req = JsonConvert.DeserializeObject<MoveRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req))
{
await moveHandler.Handle(req, userName);
}
break;
}
}
await socket.SendTextAsync("Error: Action not recognized.");
continue;
}
catch (OperationCanceledException ex)
switch (request.Action)
{
logger.LogError(ex.Message);
}
catch (WebSocketException ex)
{
logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}.");
logger.LogInformation("Probably tried writing to a closed socket.");
logger.LogError(ex.Message);
case ClientAction.ListGames:
{
var req = JsonConvert.DeserializeObject<ListGamesRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req))
{
await listGamesHandler.Handle(req, userName);
}
break;
}
case ClientAction.CreateGame:
{
var req = JsonConvert.DeserializeObject<CreateGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req))
{
await createGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.JoinGame:
{
var req = JsonConvert.DeserializeObject<JoinGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req))
{
await joinGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.JoinByCode:
{
var req = JsonConvert.DeserializeObject<JoinByCodeRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req))
{
await joinByCodeHandler.Handle(req, userName);
}
break;
}
case ClientAction.LoadGame:
{
var req = JsonConvert.DeserializeObject<LoadGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req))
{
await loadGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.Move:
{
var req = JsonConvert.DeserializeObject<MoveRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req))
{
await moveHandler.Handle(req, userName);
}
break;
}
}
}
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
return;
catch (OperationCanceledException ex)
{
logger.LogError(ex.Message);
}
catch (WebSocketException ex)
{
logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}.");
logger.LogInformation("Probably tried writing to a closed socket.");
logger.LogError(ex.Message);
}
}
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
return;
}
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
public async Task<bool> ValidateRequestAndReplyIfInvalid<TRequest>(WebSocket socket, IValidator<TRequest> validator, TRequest request)
@@ -186,7 +200,12 @@ namespace Gameboard.ShogiUI.Sockets.Services
var results = validator.Validate(request);
if (!results.IsValid)
{
await socket.SendTextAsync(string.Join('\n', results.Errors.Select(_ => _.ErrorMessage).ToString()));
var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage));
var message = JsonConvert.SerializeObject(new Response
{
Error = errors
});
await socket.SendTextAsync(message);
}
return results.IsValid;
}

View File

@@ -1,6 +1,6 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
namespace Gameboard.ShogiUI.Sockets.Managers.Utility
namespace Gameboard.ShogiUI.Sockets.Services.Utility
{
public class JsonRequest
{

View File

@@ -0,0 +1,10 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Services.Utility
{
public class Request : IRequest
{
public ClientAction Action { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
namespace Gameboard.ShogiUI.Sockets.Services.Utility
{
public class Response : IResponse
{
public string Action { get; set; }
public string Error { get; set; }
}
}

View File

@@ -3,7 +3,7 @@ using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.Services;
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -18,6 +18,7 @@ using Newtonsoft.Json.Serialization;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets
{
@@ -45,7 +46,6 @@ namespace Gameboard.ShogiUI.Sockets
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
services.AddSingleton<ISocketTokenManager, SocketTokenManager>();
services.AddSingleton<IGameboardManager, GameboardManager>();
services.AddSingleton<IActiveSessionManager, ActiveSessionManager>();
// Services
services.AddSingleton<IValidator<CreateGameRequest>, CreateGameRequestValidator>();
@@ -84,6 +84,18 @@ namespace Gameboard.ShogiUI.Sockets
options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122";
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.ValidateAudience = true;
options.Events = new JwtBearerEvents
{
OnMessageReceived = (context) =>
{
if (context.HttpContext.WebSockets.IsWebSocketRequest)
{
Console.WriteLine("Yep");
}
return Task.FromResult(0);
}
};
});
}
@@ -114,6 +126,7 @@ namespace Gameboard.ShogiUI.Sockets
.WithOrigins(origins)
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Set-Cookie")
.AllowCredentials()
)
.UseRouting()
@@ -126,12 +139,7 @@ namespace Gameboard.ShogiUI.Sockets
})
.Use(async (context, next) =>
{
var isUpgradeHeader = context
.Request
.Headers
.Any(h => h.Key.Contains("upgrade", StringComparison.InvariantCultureIgnoreCase)
&& h.Value.ToString().Contains("websocket", StringComparison.InvariantCultureIgnoreCase));
if (isUpgradeHeader)
if (context.WebSockets.IsWebSocketRequest)
{
await socketConnectionManager.HandleSocketRequest(context);
}

View File

@@ -0,0 +1,48 @@
using Gameboard.ShogiUI.Sockets.Models;
using PathFinding;
using System;
using System.Collections.Generic;
using System.Numerics;
namespace Gameboard.ShogiUI.Sockets.Utilities
{
public class CoordsToNotationCollection : Dictionary<string, Piece?>, IPlanarCollection<Piece>
{
public delegate void ForEachDelegate(Piece element, Vector2 position);
public CoordsToNotationCollection() : base(81, StringComparer.OrdinalIgnoreCase)
{
}
public CoordsToNotationCollection(Dictionary<string, Piece?> board) : base(board, StringComparer.OrdinalIgnoreCase)
{
}
public Piece? this[Vector2 vector]
{
get => this[NotationHelper.ToBoardNotation(vector)];
set => this[NotationHelper.ToBoardNotation(vector)] = value;
}
public Piece? this[int x, int y]
{
get => this[NotationHelper.ToBoardNotation(x, y)];
set => this[NotationHelper.ToBoardNotation(x, y)] = value;
}
public void ForEachNotNull(ForEachDelegate callback)
{
for (var x = 0; x < 9; x++)
{
for (var y = 0; y < 9; y++)
{
var position = new Vector2(x, y);
var elem = this[position];
if (elem != null)
callback(elem, position);
}
}
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Numerics;
using System.Text.RegularExpressions;
namespace Gameboard.ShogiUI.Sockets.Utilities
{
public static class NotationHelper
{
private static readonly string BoardNotationRegex = @"(?<file>[a-iA-I])(?<rank>[1-9])";
private static readonly char A = 'A';
public static string ToBoardNotation(Vector2 vector)
{
return ToBoardNotation((int)vector.X, (int)vector.Y);
}
public static string ToBoardNotation(int x, int y)
{
var file = (char)(x + A);
var rank = y + 1;
Console.WriteLine($"({x},{y}) - {file}{rank}");
return $"{file}{rank}";
}
public static Vector2 FromBoardNotation(string notation)
{
notation = notation.ToUpper();
if (Regex.IsMatch(notation, BoardNotationRegex))
{
var match = Regex.Match(notation, BoardNotationRegex);
char file = match.Groups["file"].Value[0];
int rank = int.Parse(match.Groups["rank"].Value);
return new Vector2(file - A, rank - 1);
}
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
}
}
}