yep
This commit is contained in:
@@ -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]
|
||||
|
||||
20
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs
Normal file
20
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||
{
|
||||
public class GetTokenResponse
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||
{
|
||||
public class PostGameInvitation
|
||||
{
|
||||
14
Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
Normal file
14
Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||
{
|
||||
public class PostSession
|
||||
{
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||
{
|
||||
public interface IRequest
|
||||
{
|
||||
ClientAction Action { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||
{
|
||||
public interface IResponse
|
||||
{
|
||||
@@ -1,9 +0,0 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces
|
||||
{
|
||||
public interface IRequest
|
||||
{
|
||||
ClientAction Action { get; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
||||
{
|
||||
public enum WhichPlayer
|
||||
{
|
||||
Player1,
|
||||
Player2
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
{
|
||||
public enum ClientAction
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
{
|
||||
public class Game
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
{
|
||||
public class Move
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
{
|
||||
public class Piece
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
{
|
||||
public enum WhichPiece
|
||||
{
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
{
|
||||
public enum WhichPlayer
|
||||
{
|
||||
Player1,
|
||||
Player2
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
18
Gameboard.ShogiUI.Sockets/Models/User.cs
Normal file
18
Gameboard.ShogiUI.Sockets/Models/User.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
|
||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
10
Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs
Normal file
10
Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
10
Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs
Normal file
10
Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs
Normal file
37
Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
22
Gameboard.ShogiUI.xUnitTests/NotationHelperShould.cs
Normal file
22
Gameboard.ShogiUI.xUnitTests/NotationHelperShould.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
594
Gameboard.ShogiUI.xUnitTests/ShogiShould.cs
Normal file
594
Gameboard.ShogiUI.xUnitTests/ShogiShould.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Gameboard.ShogiUI.xUnitTests/xunit.runner.json
Normal file
3
Gameboard.ShogiUI.xUnitTests/xunit.runner.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"methodDisplay": "method"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -7,4 +7,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user