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

@@ -10,6 +10,7 @@ namespace Benchmarking
public class Benchmarks
{
private readonly Vector2[] directions;
// Consumer is for IEnumerables.
private readonly Consumer consumer = new();
public Benchmarks()
@@ -43,9 +44,13 @@ namespace Benchmarking
//for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2));
}
//[Benchmark]
[Benchmark]
public void One()
{
for(var i=0; i<10000; i++)
{
Guid.NewGuid();
}
}
//[Benchmark]

View File

@@ -0,0 +1,20 @@
using System;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
{
public class GetGuestToken
{
}
public class GetGuestTokenResponse
{
public string PlayerName { get; }
public Guid OneTimeToken { get; }
public GetGuestTokenResponse(string playerName, Guid token)
{
PlayerName = playerName;
OneTimeToken = token;
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
{
public class GetTokenResponse
{

View File

@@ -1,21 +0,0 @@
using System;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
{
public class GetGuestToken
{
public string? ClientId { get; set; }
}
public class GetGuestTokenResponse
{
public string ClientId { get; }
public Guid OneTimeToken { get; }
public GetGuestTokenResponse(string clientId, Guid token)
{
ClientId = clientId;
OneTimeToken = token;
}
}
}

View File

@@ -1,4 +1,4 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
{
public class PostGameInvitation
{

View File

@@ -0,0 +1,14 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.ComponentModel.DataAnnotations;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
{
public class PostMove
{
[Required]
public string GameName { get; set; }
[Required]
public Move Move { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
{
public class PostSession
{

View File

@@ -1,7 +1,6 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{
public class CreateGameRequest : IRequest
{
@@ -17,9 +16,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public Game Game { get; set; }
public string PlayerName { get; set; }
public CreateGameResponse(ClientAction action)
public CreateGameResponse()
{
Action = action.ToString();
Action = ClientAction.CreateGame.ToString();
Error = string.Empty;
Game = new Game();
PlayerName = string.Empty;

View File

@@ -0,0 +1,9 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{
public interface IRequest
{
ClientAction Action { get; }
}
}

View File

@@ -1,4 +1,4 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{
public interface IResponse
{

View File

@@ -1,9 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces
{
public interface IRequest
{
ClientAction Action { get; }
}
}

View File

@@ -1,7 +1,6 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{
public class JoinByCodeRequest : IRequest
{
@@ -17,17 +16,25 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public class JoinGameResponse : IResponse
{
public string Action { get; }
public string Action { get; protected set; }
public string Error { get; set; }
public string GameName { get; set; }
public string PlayerName { get; set; }
public JoinGameResponse(ClientAction action)
public JoinGameResponse()
{
Action = action.ToString();
Action = ClientAction.JoinGame.ToString();
Error = "";
GameName = "";
PlayerName = "";
}
}
public class JoinByCodeResponse : JoinGameResponse, IResponse
{
public JoinByCodeResponse()
{
Action = ClientAction.JoinByCode.ToString();
}
}
}

View File

@@ -1,9 +1,8 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{
public class ListGamesRequest : IRequest
{
@@ -16,9 +15,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public string Error { get; set; }
public IReadOnlyList<Game> Games { get; set; }
public ListGamesResponse(ClientAction action)
public ListGamesResponse()
{
Action = action.ToString();
Action = ClientAction.ListGames.ToString();
Error = "";
Games = new Collection<Game>();
}

View File

@@ -1,8 +1,7 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{
public class LoadGameRequest : IRequest
{
@@ -19,9 +18,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public IList<Move> MoveHistory { get; set; }
public string Error { get; set; }
public LoadGameResponse(ClientAction action)
public LoadGameResponse()
{
Action = action.ToString();
Action = ClientAction.LoadGame.ToString();
}
}
}

View File

@@ -1,7 +1,6 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{
public class MoveRequest : IRequest
{
@@ -15,16 +14,16 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public string Action { get; }
public string Error { get; set; }
public string GameName { get; set; }
public BoardState BoardState { get; set; }
public string PlayerName { get; set; }
public Move Move { get; set; }
public MoveResponse(ClientAction action)
public MoveResponse()
{
Action = action.ToString();
Action = ClientAction.Move.ToString();
Error = string.Empty;
GameName = string.Empty;
BoardState = new BoardState();
PlayerName = string.Empty;
Move = new Move();
}
}
}

View File

@@ -1,8 +0,0 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
{
public enum WhichPlayer
{
Player1,
Player2
}
}

View File

@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
{
public class BoardState
{
public Piece[,] Board { get; set; } = new Piece[0, 0];
public Dictionary<string, Piece?> Board { get; set; } = new Dictionary<string, Piece?>();
public IReadOnlyCollection<Piece> Player1Hand { get; set; } = Array.Empty<Piece>();
public IReadOnlyCollection<Piece> Player2Hand { get; set; } = Array.Empty<Piece>();
public WhichPlayer? PlayerInCheck { get; set; }

View File

@@ -1,4 +1,4 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
{
public enum ClientAction
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
{
public class Game
{

View File

@@ -1,4 +1,4 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
{
public class Move
{

View File

@@ -1,4 +1,4 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
{
public class Piece
{

View File

@@ -1,4 +1,4 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
{
public enum WhichPiece
{

View File

@@ -0,0 +1,8 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
{
public enum WhichPlayer
{
Player1,
Player2
}
}

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}");
}
}
}

View File

@@ -1,44 +1,22 @@
using AutoFixture;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PathFinding;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.UnitTests.PathFinding
{
[TestClass]
public class PlanarCollectionShould
{
private class SimpleElement : IPlanarElement
{
public static int Seed { get; private set; }
public MoveSet MoveSet => null;
public bool IsUpsideDown => false;
public SimpleElement()
{
Seed = Seed++;
}
}
private Fixture fixture;
[TestInitialize]
public void TestInitialize()
{
fixture = new Fixture();
}
[TestMethod]
public void Index()
{
// Arrange
var collection = new PlanarCollection<SimpleElement>(10, 10);
var expected1 = new SimpleElement();
var expected2 = new SimpleElement();
var collection = new TestPlanarCollection();
var expected1 = new SimpleElement(1);
var expected2 = new SimpleElement(2);
// Act
collection[0, 0] = expected1;
@@ -53,40 +31,27 @@ namespace Gameboard.ShogiUI.UnitTests.PathFinding
public void Iterate()
{
// Arrange
var expected = new List<SimpleElement>();
for (var i = 0; i < 9; i++) expected.Add(new SimpleElement());
var collection = new PlanarCollection<SimpleElement>(3, 3);
for (var x = 0; x < 3; x++)
for (var y = 0; y < 3; y++)
collection[x, y] = expected[x + y];
var planarCollection = new TestPlanarCollection();
planarCollection[0, 0] = new SimpleElement(1);
planarCollection[0, 1] = new SimpleElement(2);
planarCollection[0, 2] = new SimpleElement(3);
planarCollection[1, 0] = new SimpleElement(4);
planarCollection[1, 1] = new SimpleElement(5);
// Act
var actual = new List<SimpleElement>();
foreach (var elem in collection)
foreach (var elem in planarCollection)
actual.Add(elem);
// Assert
actual.Should().BeEquivalentTo(expected);
}
[TestMethod]
public void Yep()
{
var collection = new PlanarCollection<SimpleElement>(3, 3);
collection[0, 0] = new SimpleElement();
collection[1, 0] = new SimpleElement();
collection[0, 1] = new SimpleElement();
// Act
var array2d = new SimpleElement[3, 3];
for (var x = 0; x < 3; x++)
for (var y = 0; y < 3; y++)
{
array2d[x, y] = collection[x, y];
}
Console.WriteLine("hey");
using (new AssertionScope())
{
actual[0].Number.Should().Be(1);
actual[1].Number.Should().Be(2);
actual[2].Number.Should().Be(3);
actual[3].Number.Should().Be(4);
actual[4].Number.Should().Be(5);
}
}
}
}

View File

@@ -0,0 +1,48 @@
using PathFinding;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
namespace Gameboard.ShogiUI.UnitTests.PathFinding
{
public class SimpleElement : IPlanarElement
{
public int Number { get; }
public MoveSet MoveSet => null;
public bool IsUpsideDown => false;
public SimpleElement(int number)
{
Number = number;
}
}
public class TestPlanarCollection : IPlanarCollection<SimpleElement>
{
private readonly SimpleElement[,] array;
public TestPlanarCollection()
{
array = new SimpleElement[3, 3];
}
public SimpleElement this[int x, int y]
{
get => array[x, y];
set => array[x, y] = value;
}
public SimpleElement this[Vector2 vector]
{
get => this[(int)vector.X, (int)vector.Y];
set => this[(int)vector.X, (int)vector.Y] = value;
}
public IEnumerator<SimpleElement> GetEnumerator()
{
foreach (var e in array)
yield return e;
}
//IEnumerator IEnumerable.GetEnumerator()
//{
// return array.GetEnumerator();
//}
}
}

View File

@@ -3,464 +3,13 @@ using Gameboard.ShogiUI.Sockets.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Numerics;
using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPlayer;
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPiece;
using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer;
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece;
namespace Gameboard.ShogiUI.UnitTests.Rules
{
[TestClass]
public class ShogiBoardShould
{
[TestMethod]
public void InitializeBoardState()
{
// Assert
var board = new Shogi().Board;
// Assert pieces do not start promoted.
foreach (var piece in board) piece?.IsPromoted.Should().BeFalse();
// Assert Player1.
for (var y = 0; y < 3; y++)
for (var x = 0; x < 9; x++)
board[y, x]?.Owner.Should().Be(WhichPlayer.Player2);
board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance);
board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight);
board[0, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[0, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[0, 4].WhichPiece.Should().Be(WhichPiece.King);
board[0, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[0, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[0, 7].WhichPiece.Should().Be(WhichPiece.Knight);
board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance);
board[1, 0].Should().BeNull();
board[1, 1].WhichPiece.Should().Be(WhichPiece.Rook);
for (var x = 2; x < 7; x++) board[1, x].Should().BeNull();
board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop);
board[1, 8].Should().BeNull();
for (var x = 0; x < 9; x++) board[2, x].WhichPiece.Should().Be(WhichPiece.Pawn);
// Assert empty locations.
for (var y = 3; y < 6; y++)
for (var x = 0; x < 9; x++)
board[y, x].Should().BeNull();
// Assert Player2.
for (var y = 6; y < 9; y++)
for (var x = 0; x < 9; x++)
board[y, x]?.Owner.Should().Be(WhichPlayer.Player1);
board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance);
board[8, 1].WhichPiece.Should().Be(WhichPiece.Knight);
board[8, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[8, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[8, 4].WhichPiece.Should().Be(WhichPiece.King);
board[8, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[8, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[8, 7].WhichPiece.Should().Be(WhichPiece.Knight);
board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance);
board[7, 0].Should().BeNull();
board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop);
for (var x = 2; x < 7; x++) board[7, x].Should().BeNull();
board[7, 7].WhichPiece.Should().Be(WhichPiece.Rook);
board[7, 8].Should().BeNull();
for (var x = 0; x < 9; x++) board[6, x].WhichPiece.Should().Be(WhichPiece.Pawn);
}
[TestMethod]
public void InitializeBoardStateWithMoves()
{
var moves = new[]
{
// Pawn
new Move(new Vector2(0, 6), new Vector2(0, 5))
};
var shogi = new Shogi(moves);
shogi.Board[6, 0].Should().BeNull();
shogi.Board[5, 0].WhichPiece.Should().Be(WhichPiece.Pawn);
}
[TestMethod]
public void PreventInvalidMoves_MoveFromEmptyPosition()
{
// Arrange
var shogi = new Shogi();
// Prerequisit
shogi.Board[4, 4].Should().BeNull();
// Act
var moveSuccess = shogi.Move(new Move(new Vector2(4, 4), new Vector2(4, 5)));
// Assert
moveSuccess.Should().BeFalse();
shogi.Board[4, 4].Should().BeNull();
shogi.Board[5, 4].Should().BeNull();
}
[TestMethod]
public void PreventInvalidMoves_MoveToCurrentPosition()
{
// Arrange
var shogi = new Shogi();
// Act - P1 "moves" pawn to the position it already exists at.
var moveSuccess = shogi.Move(new Move(new Vector2(0, 6), new Vector2(0, 6)));
// Assert
moveSuccess.Should().BeFalse();
shogi.Board[6, 0].WhichPiece.Should().Be(WhichPiece.Pawn);
}
[TestMethod]
public void PreventInvalidMoves_MoveSet()
{
// Bishop moving lateral
var invalidLanceMove = new Move(new Vector2(1, 1), new Vector2(2, 1));
var shogi = new Shogi();
var moveSuccess = shogi.Move(invalidLanceMove);
moveSuccess.Should().BeFalse();
// Assert the Lance has not actually moved.
shogi.Board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance);
}
[TestMethod]
public void PreventInvalidMoves_Ownership()
{
// Arrange
var shogi = new Shogi();
shogi.WhoseTurn.Should().Be(WhichPlayer.Player1);
shogi.Board[2, 8].Owner.Should().Be(WhichPlayer.Player2);
// Act - Move Player2 Pawn when it's Player1 turn.
var moveSuccess = shogi.Move(new Move(new Vector2(8, 2), new Vector2(8, 3)));
// Assert
moveSuccess.Should().BeFalse();
shogi.Board[6, 8].WhichPiece.Should().Be(WhichPiece.Pawn);
shogi.Board[5, 8].Should().BeNull();
}
[TestMethod]
public void PreventInvalidMoves_MoveThroughAllies()
{
// Lance moving through the pawn before it.
var invalidLanceMove = new Move(new Vector2(0, 8), new Vector2(0, 4));
var shogi = new Shogi();
var moveSuccess = shogi.Move(invalidLanceMove);
moveSuccess.Should().BeFalse();
// Assert the Lance has not actually moved.
shogi.Board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance);
}
[TestMethod]
public void PreventInvalidMoves_CaptureAlly()
{
// Knight capturing allied Pawn
var invalidKnightMove = new Move(new Vector2(1, 8), new Vector2(0, 6));
var shogi = new Shogi();
var moveSuccess = shogi.Move(invalidKnightMove);
moveSuccess.Should().BeFalse();
// Assert the Knight has not actually moved or captured.
shogi.Board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight);
shogi.Board[2, 0].WhichPiece.Should().Be(WhichPiece.Pawn);
}
[TestMethod]
public void PreventInvalidMoves_Check()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move(new Vector2(2, 6), new Vector2(2, 5)),
// P2 Pawn
new Move(new Vector2(6, 2), new Vector2(6, 3)),
// P1 Bishop puts P2 in check
new Move(new Vector2(1, 7), new Vector2(6, 2))
};
var shogi = new Shogi(moves);
// Prerequisit
shogi.InCheck.Should().Be(WhichPlayer.Player2);
// Act - P2 moves Lance while remaining in check.
var moveSuccess = shogi.Move(new Move(new Vector2(0, 8), new Vector2(0, 7)));
// Assert
moveSuccess.Should().BeFalse();
shogi.InCheck.Should().Be(WhichPlayer.Player2);
shogi.Board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance);
shogi.Board[7, 8].Should().BeNull();
}
[TestMethod]
public void PreventInvalidDrops_MoveSet()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move(new Vector2(2, 6), new Vector2(2, 5) ),
// P2 Pawn
new Move(new Vector2(0, 2), new Vector2(0, 3) ),
// P1 Bishop takes P2 Pawn
new Move(new Vector2(1, 7), new Vector2(6, 2) ),
// P2 Gold, block check from P1 Bishop.
new Move(new Vector2(5, 0), new Vector2(5, 1) ),
// P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance
new Move(new Vector2(6, 2), new Vector2(7, 1), true ),
// P2 Pawn again
new Move(new Vector2(0, 3), new Vector2(0, 4) ),
// P1 Bishop takes P2 Knight
new Move(new Vector2(7, 1), new Vector2(7, 0) ),
// P2 Pawn again
new Move(new Vector2(0, 4), new Vector2(0, 5) ),
// P1 Bishop takes P2 Lance
new Move(new Vector2(7, 0), new Vector2(8, 0) ),
// P2 Lance (move to make room for attempted P1 Pawn placement)
new Move(new Vector2(0, 0), new Vector2(0, 1) ),
// P1 arbitrary move
new Move(new Vector2(4, 8), new Vector2(4, 7) ),
// P2 Pawn again, takes P1 Pawn
new Move(new Vector2(0, 5) , new Vector2(0, 6) ),
};
var shogi = new Shogi(moves);
// Prerequisites
shogi.Hands[WhichPlayer.Player1].Count.Should().Be(4);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
// Act | Assert - It is P1 turn
/// try illegally placing Knight from the hand.
shogi.Board[0, 7].Should().BeNull();
var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 0)));
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[0, 7].Should().BeNull();
dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 1)));
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[1, 7].Should().BeNull();
/// try illegally placing Pawn from the hand
dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, new Vector2(7, 0)));
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
shogi.Board[0, 7].Should().BeNull();
/// try illegally placing Lance from the hand
dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(7, 0)));
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[0, 7].Should().BeNull();
}
[TestMethod]
public void PreventInvalidDrop_Check()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move(new Vector2(2, 6), new Vector2(2, 5)),
// P2 Pawn
new Move(new Vector2(8, 2), new Vector2(8, 3)),
// P1 Bishop, check
new Move(new Vector2(1, 7), new Vector2(6, 2)),
// P2 Gold, block check
new Move(new Vector2(5, 0), new Vector2(5, 1)),
// P1 arbitrary move
new Move(new Vector2(0, 6), new Vector2(0, 5)),
// P2 Bishop
new Move(new Vector2(7, 1), new Vector2(8, 2)),
// P1 Bishop takes P2 Lance
new Move(new Vector2(6, 2), new Vector2(8, 0)),
// P2 Bishop
new Move(new Vector2(8, 2), new Vector2(7, 1)),
// P1 arbitrary move
new Move(new Vector2(0, 5), new Vector2(0, 4)),
// P2 Bishop, check
new Move(new Vector2(7, 1), new Vector2(2, 6)),
};
var shogi = new Shogi(moves);
// Prerequisites
shogi.InCheck.Should().Be(WhichPlayer.Player1);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
// Act - P1 tries to place a Lance while in check.
var dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(4, 4)));
// Assert
dropSuccess.Should().BeFalse();
shogi.Board[4, 4].Should().BeNull();
shogi.InCheck.Should().Be(WhichPlayer.Player1);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
}
[TestMethod]
public void PreventInvalidDrop_Capture()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move(new Vector2(2, 6), new Vector2(2, 5)),
// P2 Pawn
new Move(new Vector2(6, 2), new Vector2(6, 3)),
// P1 Bishop, capture P2 Pawn, check
new Move(new Vector2(1, 7), new Vector2(6, 2)),
// P2 Gold, block check
new Move(new Vector2(5, 0), new Vector2(5, 1)),
// P1 Bishop capture P2 Bishop
new Move(new Vector2(6, 2), new Vector2(7, 1)),
// P2 arbitrary move
new Move(new Vector2(0, 0), new Vector2(0, 1)),
};
var shogi = new Shogi(moves);
// Prerequisites
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.Board[0, 4].Should().NotBeNull();
// Act - P1 tries to place Bishop from hand to an already-occupied position
var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, new Vector2(4, 0)));
// Assert
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.Board[0, 4].WhichPiece.Should().Be(WhichPiece.King);
}
[TestMethod]
public void Check()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move(new Vector2(2, 6), new Vector2(2, 5) ),
// P2 Pawn
new Move(new Vector2(6, 2), new Vector2(6, 3) ),
};
var shogi = new Shogi(moves);
// Act - P1 Bishop, check
shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2)));
// Assert
shogi.InCheck.Should().Be(WhichPlayer.Player2);
}
[TestMethod]
public void Capture()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move(new Vector2(2, 6), new Vector2(2, 5)),
// P2 Pawn
new Move(new Vector2(6, 2), new Vector2(6, 3))
};
var shogi = new Shogi(moves);
// Act - P1 Bishop captures P2 Bishop
var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(7, 1)));
// Assert
moveSuccess.Should().BeTrue();
shogi.Board
.Cast<Piece>()
.Count(piece => piece?.WhichPiece == WhichPiece.Bishop)
.Should()
.Be(1);
shogi.Board[7, 1].Should().BeNull();
shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop);
shogi.Hands[WhichPlayer.Player1]
.Should()
.ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1);
// Act - P2 Silver captures P1 Bishop
moveSuccess = shogi.Move(new Move(new Vector2(6, 0), new Vector2(7, 1)));
// Assert
moveSuccess.Should().BeTrue();
shogi.Board[0, 6].Should().BeNull();
shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
shogi.Board
.Cast<Piece>()
.Count(piece => piece?.WhichPiece == WhichPiece.Bishop)
.Should().Be(0);
shogi.Hands[WhichPlayer.Player2]
.Should()
.ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player2);
}
[TestMethod]
public void Promote()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move(new Vector2(2, 6), new Vector2(2, 5) ),
// P2 Pawn
new Move(new Vector2(6, 2), new Vector2(6, 3) )
};
var shogi = new Shogi(moves);
// Act - P1 moves across promote threshold.
var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2), true));
// Assert
moveSuccess.Should().BeTrue();
shogi.Board[7, 1].Should().BeNull();
shogi.Board[2, 6].Should().Match<Piece>(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true);
}
[TestMethod]
public void CheckMate()
{
// Arrange
var moves = new[]
{
// P1 Rook
new Move(new Vector2(7, 7), new Vector2(4, 7) ),
// P2 Gold
new Move(new Vector2(3, 0), new Vector2(2, 1) ),
// P1 Pawn
new Move(new Vector2(4, 6), new Vector2(4, 5) ),
// P2 other Gold
new Move(new Vector2(5, 0), new Vector2(6, 1) ),
// P1 same Pawn
new Move(new Vector2(4, 5), new Vector2(4, 4) ),
// P2 Pawn
new Move(new Vector2(4, 2), new Vector2(4, 3) ),
// P1 Pawn takes P2 Pawn
new Move(new Vector2(4, 4), new Vector2(4, 3) ),
// P2 King
new Move(new Vector2(4, 0), new Vector2(4, 1) ),
// P1 Pawn promotes, threatens P2 King
new Move(new Vector2(4, 3), new Vector2(4, 2), true ),
// P2 King retreat
new Move(new Vector2(4, 1), new Vector2(4, 0) ),
};
var shogi = new Shogi(moves);
// Act - P1 Pawn wins by checkmate.
var moveSuccess = shogi.Move(new Move(new Vector2(4, 2), new Vector2(4, 1)));
// Assert - checkmate
moveSuccess.Should().BeTrue();
shogi.IsCheckmate.Should().BeTrue();
}
}
}

View File

@@ -0,0 +1,41 @@
using AutoFixture;
using FluentAssertions;
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Utilities;
using Xunit;
namespace Gameboard.ShogiUI.xUnitTests
{
public class CoordsToNotationCollectionShould
{
private readonly Fixture fixture;
private readonly CoordsToNotationCollection collection;
public CoordsToNotationCollectionShould()
{
fixture = new Fixture();
collection = new CoordsToNotationCollection();
}
[Fact]
public void TranslateCoordinatesToNotation()
{
// Arrange
collection[0, 0] = fixture.Create<Piece>();
collection[4, 4] = fixture.Create<Piece>();
collection[8, 8] = fixture.Create<Piece>();
collection[2, 2] = fixture.Create<Piece>();
// Assert
collection["A1"].Should().BeSameAs(collection[0, 0]);
collection["E5"].Should().BeSameAs(collection[4, 4]);
collection["I9"].Should().BeSameAs(collection[8, 8]);
collection["C3"].Should().BeSameAs(collection[2, 2]);
}
[Fact]
public void Yep()
{
}
}
}

View File

@@ -1,5 +1,5 @@
using FluentAssertions;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Xunit;
namespace Gameboard.ShogiUI.xUnitTests

View File

@@ -25,4 +25,10 @@
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
using FluentAssertions;
using Gameboard.ShogiUI.Sockets.Utilities;
using System.Numerics;
using Xunit;
namespace Gameboard.ShogiUI.xUnitTests
{
public class NotationHelperShould
{
[Fact]
public void TranslateVectorsToNotation()
{
NotationHelper.ToBoardNotation(2, 2).Should().Be("C3");
}
[Fact]
public void TranslateNotationToVectors()
{
NotationHelper.FromBoardNotation("C3").Should().Be(new Vector2(2, 2));
}
}
}

View File

@@ -1,8 +1,8 @@
using AutoFixture;
using FluentAssertions;
using FluentAssertions.Execution;
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 Gameboard.ShogiUI.Sockets.Services.RequestValidators;
using Xunit;

View File

@@ -0,0 +1,594 @@
using FluentAssertions;
using FluentAssertions.Execution;
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Models;
using System.Linq;
using Xunit;
using Xunit.Abstractions;
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece;
using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer;
namespace Gameboard.ShogiUI.xUnitTests
{
public class ShogiShould
{
private readonly ITestOutputHelper output;
public ShogiShould(ITestOutputHelper output)
{
this.output = output;
}
[Fact]
public void InitializeBoardState()
{
// Act
var board = new Shogi().Board;
// Assert
board["A1"].WhichPiece.Should().Be(WhichPiece.Lance);
board["A1"].Owner.Should().Be(WhichPlayer.Player1);
board["A1"].IsPromoted.Should().Be(false);
board["B1"].WhichPiece.Should().Be(WhichPiece.Knight);
board["B1"].Owner.Should().Be(WhichPlayer.Player1);
board["B1"].IsPromoted.Should().Be(false);
board["C1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board["C1"].Owner.Should().Be(WhichPlayer.Player1);
board["C1"].IsPromoted.Should().Be(false);
board["D1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board["D1"].Owner.Should().Be(WhichPlayer.Player1);
board["D1"].IsPromoted.Should().Be(false);
board["E1"].WhichPiece.Should().Be(WhichPiece.King);
board["E1"].Owner.Should().Be(WhichPlayer.Player1);
board["E1"].IsPromoted.Should().Be(false);
board["F1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board["F1"].Owner.Should().Be(WhichPlayer.Player1);
board["F1"].IsPromoted.Should().Be(false);
board["G1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board["G1"].Owner.Should().Be(WhichPlayer.Player1);
board["G1"].IsPromoted.Should().Be(false);
board["H1"].WhichPiece.Should().Be(WhichPiece.Knight);
board["H1"].Owner.Should().Be(WhichPlayer.Player1);
board["H1"].IsPromoted.Should().Be(false);
board["I1"].WhichPiece.Should().Be(WhichPiece.Lance);
board["I1"].Owner.Should().Be(WhichPlayer.Player1);
board["I1"].IsPromoted.Should().Be(false);
board["A2"].Should().BeNull();
board["B2"].WhichPiece.Should().Be(WhichPiece.Bishop);
board["B2"].Owner.Should().Be(WhichPlayer.Player1);
board["B2"].IsPromoted.Should().Be(false);
board["C2"].Should().BeNull();
board["D2"].Should().BeNull();
board["E2"].Should().BeNull();
board["F2"].Should().BeNull();
board["G2"].Should().BeNull();
board["H2"].WhichPiece.Should().Be(WhichPiece.Rook);
board["H2"].Owner.Should().Be(WhichPlayer.Player1);
board["H2"].IsPromoted.Should().Be(false);
board["I2"].Should().BeNull();
board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["A3"].Owner.Should().Be(WhichPlayer.Player1);
board["A3"].IsPromoted.Should().Be(false);
board["B3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["B3"].Owner.Should().Be(WhichPlayer.Player1);
board["B3"].IsPromoted.Should().Be(false);
board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["C3"].Owner.Should().Be(WhichPlayer.Player1);
board["C3"].IsPromoted.Should().Be(false);
board["D3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["D3"].Owner.Should().Be(WhichPlayer.Player1);
board["D3"].IsPromoted.Should().Be(false);
board["E3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["E3"].Owner.Should().Be(WhichPlayer.Player1);
board["E3"].IsPromoted.Should().Be(false);
board["F3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["F3"].Owner.Should().Be(WhichPlayer.Player1);
board["F3"].IsPromoted.Should().Be(false);
board["G3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["G3"].Owner.Should().Be(WhichPlayer.Player1);
board["G3"].IsPromoted.Should().Be(false);
board["H3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["H3"].Owner.Should().Be(WhichPlayer.Player1);
board["H3"].IsPromoted.Should().Be(false);
board["I3"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["I3"].Owner.Should().Be(WhichPlayer.Player1);
board["I3"].IsPromoted.Should().Be(false);
board["A4"].Should().BeNull();
board["B4"].Should().BeNull();
board["C4"].Should().BeNull();
board["D4"].Should().BeNull();
board["E4"].Should().BeNull();
board["F4"].Should().BeNull();
board["G4"].Should().BeNull();
board["H4"].Should().BeNull();
board["I4"].Should().BeNull();
board["A5"].Should().BeNull();
board["B5"].Should().BeNull();
board["C5"].Should().BeNull();
board["D5"].Should().BeNull();
board["E5"].Should().BeNull();
board["F5"].Should().BeNull();
board["G5"].Should().BeNull();
board["H5"].Should().BeNull();
board["I5"].Should().BeNull();
board["A6"].Should().BeNull();
board["B6"].Should().BeNull();
board["C6"].Should().BeNull();
board["D6"].Should().BeNull();
board["E6"].Should().BeNull();
board["F6"].Should().BeNull();
board["G6"].Should().BeNull();
board["H6"].Should().BeNull();
board["I6"].Should().BeNull();
board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["A7"].Owner.Should().Be(WhichPlayer.Player2);
board["A7"].IsPromoted.Should().Be(false);
board["B7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["B7"].Owner.Should().Be(WhichPlayer.Player2);
board["B7"].IsPromoted.Should().Be(false);
board["C7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["C7"].Owner.Should().Be(WhichPlayer.Player2);
board["C7"].IsPromoted.Should().Be(false);
board["D7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["D7"].Owner.Should().Be(WhichPlayer.Player2);
board["D7"].IsPromoted.Should().Be(false);
board["E7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["E7"].Owner.Should().Be(WhichPlayer.Player2);
board["E7"].IsPromoted.Should().Be(false);
board["F7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["F7"].Owner.Should().Be(WhichPlayer.Player2);
board["F7"].IsPromoted.Should().Be(false);
board["G7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["G7"].Owner.Should().Be(WhichPlayer.Player2);
board["G7"].IsPromoted.Should().Be(false);
board["H7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["H7"].Owner.Should().Be(WhichPlayer.Player2);
board["H7"].IsPromoted.Should().Be(false);
board["I7"].WhichPiece.Should().Be(WhichPiece.Pawn);
board["I7"].Owner.Should().Be(WhichPlayer.Player2);
board["I7"].IsPromoted.Should().Be(false);
board["A8"].Should().BeNull();
board["B8"].WhichPiece.Should().Be(WhichPiece.Rook);
board["B8"].Owner.Should().Be(WhichPlayer.Player2);
board["B8"].IsPromoted.Should().Be(false);
board["C8"].Should().BeNull();
board["D8"].Should().BeNull();
board["E8"].Should().BeNull();
board["F8"].Should().BeNull();
board["G8"].Should().BeNull();
board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop);
board["H8"].Owner.Should().Be(WhichPlayer.Player2);
board["H8"].IsPromoted.Should().Be(false);
board["I8"].Should().BeNull();
board["A9"].WhichPiece.Should().Be(WhichPiece.Lance);
board["A9"].Owner.Should().Be(WhichPlayer.Player2);
board["A9"].IsPromoted.Should().Be(false);
board["B9"].WhichPiece.Should().Be(WhichPiece.Knight);
board["B9"].Owner.Should().Be(WhichPlayer.Player2);
board["B9"].IsPromoted.Should().Be(false);
board["C9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board["C9"].Owner.Should().Be(WhichPlayer.Player2);
board["C9"].IsPromoted.Should().Be(false);
board["D9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board["D9"].Owner.Should().Be(WhichPlayer.Player2);
board["D9"].IsPromoted.Should().Be(false);
board["E9"].WhichPiece.Should().Be(WhichPiece.King);
board["E9"].Owner.Should().Be(WhichPlayer.Player2);
board["E9"].IsPromoted.Should().Be(false);
board["F9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board["F9"].Owner.Should().Be(WhichPlayer.Player2);
board["F9"].IsPromoted.Should().Be(false);
board["G9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board["G9"].Owner.Should().Be(WhichPlayer.Player2);
board["G9"].IsPromoted.Should().Be(false);
board["H9"].WhichPiece.Should().Be(WhichPiece.Knight);
board["H9"].Owner.Should().Be(WhichPlayer.Player2);
board["H9"].IsPromoted.Should().Be(false);
board["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
board["I9"].Owner.Should().Be(WhichPlayer.Player2);
board["I9"].IsPromoted.Should().Be(false);
}
[Fact]
public void InitializeBoardStateWithMoves()
{
var moves = new[]
{
// P1 Pawn
new Move("A3", "A4")
};
var shogi = new Shogi(moves);
shogi.Board["A3"].Should().BeNull();
shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn);
}
[Fact]
public void PreventInvalidMoves_MoveFromEmptyPosition()
{
// Arrange
var shogi = new Shogi();
shogi.Board["D5"].Should().BeNull();
// Act
var moveSuccess = shogi.Move(new Move("D5", "D6"));
// Assert
moveSuccess.Should().BeFalse();
shogi.Board["D5"].Should().BeNull();
shogi.Board["D6"].Should().BeNull();
}
[Fact]
public void PreventInvalidMoves_MoveToCurrentPosition()
{
// Arrange
var shogi = new Shogi();
// Act - P1 "moves" pawn to the position it already exists at.
var moveSuccess = shogi.Move(new Move("A3", "A3"));
// Assert
moveSuccess.Should().BeFalse();
shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn);
shogi.Player1Hand.Should().BeEmpty();
shogi.Player2Hand.Should().BeEmpty();
}
[Fact]
public void PreventInvalidMoves_MoveSet()
{
// Arrange
var shogi = new Shogi();
// Act - Move Lance illegally
var moveSuccess = shogi.Move(new Move("A1", "D5"));
// Assert
moveSuccess.Should().BeFalse();
shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance);
shogi.Board["A5"].Should().BeNull();
shogi.Player1Hand.Should().BeEmpty();
shogi.Player2Hand.Should().BeEmpty();
}
[Fact]
public void PreventInvalidMoves_Ownership()
{
// Arrange
var shogi = new Shogi();
shogi.WhoseTurn.Should().Be(WhichPlayer.Player1);
shogi.Board["A7"].Owner.Should().Be(WhichPlayer.Player2);
// Act - Move Player2 Pawn when it is Player1 turn.
var moveSuccess = shogi.Move(new Move("A7", "A6"));
// Assert
moveSuccess.Should().BeFalse();
shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn);
shogi.Board["A6"].Should().BeNull();
}
[Fact]
public void PreventInvalidMoves_MoveThroughAllies()
{
// Arrange
var shogi = new Shogi();
// Act - Move P1 Lance through P1 Pawn.
var moveSuccess = shogi.Move(new Move("A1", "A5"));
// Assert
moveSuccess.Should().BeFalse();
shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance);
shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn);
shogi.Board["A5"].Should().BeNull();
}
[Fact]
public void PreventInvalidMoves_CaptureAlly()
{
// Arrange
var shogi = new Shogi();
// Act - P1 Knight tries to capture P1 Pawn.
var moveSuccess = shogi.Move(new Move("B1", "C3"));
// Arrange
moveSuccess.Should().BeFalse();
shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight);
shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn);
shogi.Player1Hand.Should().BeEmpty();
shogi.Player2Hand.Should().BeEmpty();
}
[Fact]
public void PreventInvalidMoves_Check()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move("C3", "C4"),
// P2 Pawn
new Move("G7", "G6"),
// P1 Bishop puts P2 in check
new Move("B2", "G7")
};
var shogi = new Shogi(moves);
shogi.InCheck.Should().Be(WhichPlayer.Player2);
// Act - P2 moves Lance while in check.
var moveSuccess = shogi.Move(new Move("I9", "I8"));
// Assert
moveSuccess.Should().BeFalse();
shogi.InCheck.Should().Be(WhichPlayer.Player2);
shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
shogi.Board["I8"].Should().BeNull();
}
[Fact]
public void PreventInvalidDrops_MoveSet()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move("C3", "C4"),
// P2 Pawn
new Move("I7", "I6"),
// P1 Bishop takes P2 Pawn.
new Move("B2", "G7"),
// P2 Gold, block check from P1 Bishop.
new Move("F9", "F8"),
// P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance
new Move("G7", "H8", true),
// P2 Pawn again
new Move("I6", "I5"),
// P1 Bishop takes P2 Knight
new Move("H8", "H9"),
// P2 Pawn again
new Move("I5", "I4"),
// P1 Bishop takes P2 Lance
new Move("H9", "I9"),
// P2 Pawn captures P1 Pawn
new Move("I4", "I3")
};
var shogi = new Shogi(moves);
shogi.Player1Hand.Count.Should().Be(4);
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.WhoseTurn.Should().Be(WhichPlayer.Player1);
// Act | Assert - Illegally placing Knight from the hand in farthest row.
shogi.Board["H9"].Should().BeNull();
var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9"));
dropSuccess.Should().BeFalse();
shogi.Board["H9"].Should().BeNull();
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
// Act | Assert - Illegally placing Knight from the hand in second farthest row.
shogi.Board["H8"].Should().BeNull();
dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8"));
dropSuccess.Should().BeFalse();
shogi.Board["H8"].Should().BeNull();
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
// Act | Assert - Illegally place Lance from the hand.
shogi.Board["H9"].Should().BeNull();
dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9"));
dropSuccess.Should().BeFalse();
shogi.Board["H9"].Should().BeNull();
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
// Act | Assert - Illegally place Pawn from the hand.
shogi.Board["H9"].Should().BeNull();
dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9"));
dropSuccess.Should().BeFalse();
shogi.Board["H9"].Should().BeNull();
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
// Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn.
// TODO
}
[Fact]
public void PreventInvalidDrop_Check()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move("C3", "C4"),
// P2 Pawn
new Move("G7", "G6"),
// P1 Pawn, arbitrary move.
new Move("A3", "A4"),
// P2 Bishop takes P1 Bishop
new Move("H8", "B2"),
// P1 Silver takes P2 Bishop
new Move("C1", "B2"),
// P2 Pawn, arbtrary move
new Move("A7", "A6"),
// P1 drop Bishop, place P2 in check
new Move(WhichPiece.Bishop, "G7")
};
var shogi = new Shogi(moves);
shogi.InCheck.Should().Be(WhichPlayer.Player2);
shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.Board["E5"].Should().BeNull();
// Act - P2 places a Bishop while in check.
var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5"));
// Assert
dropSuccess.Should().BeFalse();
shogi.Board["E5"].Should().BeNull();
shogi.InCheck.Should().Be(WhichPlayer.Player2);
shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
}
[Fact]
public void PreventInvalidDrop_Capture()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move("C3", "C4"),
// P2 Pawn
new Move("G7", "G6"),
// P1 Bishop capture P2 Bishop
new Move("B2", "H8"),
// P2 Pawn
new Move("G6", "G5")
};
var shogi = new Shogi(moves);
using (new AssertionScope())
{
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.Board["I9"].Should().NotBeNull();
shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2);
}
// Act - P1 tries to place a piece where an opponent's piece resides.
var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9"));
// Assert
using (new AssertionScope())
{
dropSuccess.Should().BeFalse();
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.Board["I9"].Should().NotBeNull();
shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
shogi.Board["I9"].Owner.Should().Be(WhichPlayer.Player2);
}
}
[Fact]
public void Check()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move("C3", "C4"),
// P2 Pawn
new Move("G7", "G6"),
};
var shogi = new Shogi(moves);
// Act - P1 Bishop, check
shogi.Move(new Move("B2", "G7"));
// Assert
shogi.InCheck.Should().Be(WhichPlayer.Player2);
}
[Fact]
public void Promote()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move("C3", "C4" ),
// P2 Pawn
new Move("G7", "G6" )
};
var shogi = new Shogi(moves);
// Act - P1 moves across promote threshold.
var moveSuccess = shogi.Move(new Move("B2", "G7", true));
// Assert
using (new AssertionScope())
{
moveSuccess.Should().BeTrue();
shogi.Board["B2"].Should().BeNull();
shogi.Board["G7"].Should().NotBeNull();
shogi.Board["G7"].WhichPiece.Should().Be(WhichPiece.Bishop);
shogi.Board["G7"].Owner.Should().Be(WhichPlayer.Player1);
shogi.Board["G7"].IsPromoted.Should().BeTrue();
}
}
[Fact]
public void CheckMate()
{
// Arrange
var moves = new[]
{
// P1 Rook
new Move("H2", "E2"),
// P2 Gold
new Move("F9", "G8"),
// P1 Pawn
new Move("E3", "E4"),
// P2 other Gold
new Move("D9", "C8"),
// P1 same Pawn
new Move("E4", "E5"),
// P2 Pawn
new Move("E7", "E6"),
// P1 Pawn takes P2 Pawn
new Move("E5", "E6"),
// P2 King
new Move("E9", "E8"),
// P1 Pawn promotes, threatens P2 King
new Move("E6", "E7", true),
// P2 King retreat
new Move("E8", "E9"),
};
var shogi = new Shogi(moves);
output.WriteLine(shogi.PrintStateAsAscii());
// Act - P1 Pawn wins by checkmate.
var moveSuccess = shogi.Move(new Move("E7", "E8"));
output.WriteLine(shogi.PrintStateAsAscii());
// Assert - checkmate
moveSuccess.Should().BeTrue();
shogi.IsCheckmate.Should().BeTrue();
shogi.InCheck.Should().Be(WhichPlayer.Player2);
}
[Fact]
public void Capture()
{
// Arrange
var moves = new[]
{
new Move("C3", "C4"),
new Move("G7", "G6")
};
var shogi = new Shogi(moves);
// Act - P1 Bishop captures P2 Bishop
var moveSuccess = shogi.Move(new Move("B2", "H8"));
// Assert
moveSuccess.Should().BeTrue();
shogi.Board["B2"].Should().BeNull();
shogi.Board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop);
shogi.Board["H8"].Owner.Should().Be(WhichPlayer.Player1);
shogi.Board.Values
.Where(p => p != null)
.Should().ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop);
shogi.Player1Hand
.Should()
.ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1);
}
}
}

View File

@@ -0,0 +1,3 @@
{
"methodDisplay": "method"
}

View File

@@ -4,15 +4,15 @@ namespace PathFinding
{
public static class Direction
{
public static readonly Vector2 Up = new(0, -1);
public static readonly Vector2 Down = new(0, 1);
public static readonly Vector2 Up = new(0, 1);
public static readonly Vector2 Down = new(0, -1);
public static readonly Vector2 Left = new(-1, 0);
public static readonly Vector2 Right = new(1, 0);
public static readonly Vector2 UpLeft = new(-1, -1);
public static readonly Vector2 UpRight = new(1, -1);
public static readonly Vector2 DownLeft = new(-1, 1);
public static readonly Vector2 DownRight = new(1, 1);
public static readonly Vector2 KnightLeft = new(-1, -2);
public static readonly Vector2 KnightRight = new(1, -2);
public static readonly Vector2 UpLeft = new(-1, 1);
public static readonly Vector2 UpRight = new(1, 1);
public static readonly Vector2 DownLeft = new(-1, -1);
public static readonly Vector2 DownRight = new(1, -1);
public static readonly Vector2 KnightLeft = new(-1, 2);
public static readonly Vector2 KnightRight = new(1, 2);
}
}

View File

@@ -1,10 +1,11 @@
using System.Collections.Generic;
using System.Numerics;
namespace PathFinding
{
public interface IPlanarCollection<T> : IEnumerable<T> where T : IPlanarElement
public interface IPlanarCollection<T> where T : IPlanarElement
{
T? this[float x, float y] { get; set; }
int GetLength(int dimension);
T? this[Vector2 vector] { get; set; }
T? this[int x, int y] { get; set; }
}
}

View File

@@ -14,11 +14,14 @@ namespace PathFinding
private readonly IPlanarCollection<T> collection;
private readonly int width;
private readonly int height;
public PathFinder2D(IPlanarCollection<T> collection)
/// <param name="width">Horizontal size, in steps, of the pathable plane.</param>
/// <param name="height">Vertical size, in steps, of the pathable plane.</param>
public PathFinder2D(IPlanarCollection<T> collection, int width, int height)
{
this.collection = collection;
width = collection.GetLength(0);
height = collection.GetLength(1);
this.width = width;
this.height = height;
}
/// <summary>
@@ -29,13 +32,13 @@ namespace PathFinding
/// <param name="destination">The destination.</param>
/// <param name="callback">Do cool stuff here.</param>
/// <returns>True if the element reached the destination.</returns>
public bool PathTo(Vector2 origin, Vector2 destination, Callback callback = null)
public bool PathTo(Vector2 origin, Vector2 destination, Callback? callback = null)
{
if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0)
{
return false;
}
var element = collection[origin.Y, origin.X];
var element = collection[origin];
if (element == null) return false;
var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination);
@@ -50,7 +53,7 @@ namespace PathFinding
while (shouldPath && next != destination)
{
next = Vector2.Add(next, path.Direction);
var collider = collection[(int)next.Y, (int)next.X];
var collider = collection[next];
if (collider != null)
{
callback?.Invoke(collider, next);
@@ -66,7 +69,7 @@ namespace PathFinding
public void PathEvery(Vector2 from, Callback callback)
{
var element = collection[from.Y, from.X];
var element = collection[from];
if (element == null)
{
Console.WriteLine("Null element in PathEvery");
@@ -103,7 +106,7 @@ namespace PathFinding
var next = Vector2.Add(origin, direction);
while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height)
{
var element = collection[next.Y, next.X];
var element = collection[next];
if (element != null) callback(element, next);
next = Vector2.Add(next, direction);
}

View File

@@ -7,4 +7,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,61 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace PathFinding
{
// TODO: Get rid of this thing in favor of T[,] multi-dimensional array with extension methods.
public class PlanarCollection<T> : IPlanarCollection<T>, IEnumerable<T> where T : IPlanarElement
{
public delegate void ForEachDelegate(T element, int x, int y);
private readonly T?[] array;
private readonly int width;
private readonly int height;
public PlanarCollection(int width, int height)
{
this.width = width;
this.height = height;
array = new T[width * height];
}
public T? this[int y, int x]
{
get => array[y * width + x];
set => array[y * width + x] = value;
}
public T? this[float y, float x]
{
get => array[(int)y * width + (int)x];
set => array[(int)y * width + (int)x] = value;
}
public int GetLength(int dimension) => dimension switch
{
0 => height,
1 => width,
_ => throw new IndexOutOfRangeException()
};
public void ForEachNotNull(ForEachDelegate callback)
{
for (var x = 0; x < width; x++)
{
for (var y = 0; y < height; y++)
{
var elem = this[y, x];
if (elem != null)
callback(elem, x, y);
}
}
}
public IEnumerator<T> GetEnumerator()
{
foreach (var item in array)
if (item != null) yield return item;
}
IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator();
}
}