mapper class and delete old stuff

This commit is contained in:
2022-06-12 21:45:46 -05:00
parent 4ca0b63564
commit ab8d0c4c7c
39 changed files with 325 additions and 818 deletions

View File

@@ -1,11 +1,11 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.Collections.ObjectModel; using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
{ {
public class GetSessionsResponse public class GetSessionsResponse
{ {
public Collection<Session> PlayerHasJoinedSessions { get; set; } public IList<SessionMetadata> PlayerHasJoinedSessions { get; set; }
public Collection<Session> AllOtherSessions { get; set; } public IList<SessionMetadata> AllOtherSessions { get; set; }
} }
} }

View File

@@ -1,12 +1,11 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{ {
public class CreateGameResponse : IResponse public class CreateGameResponse : ISocketResponse
{ {
public string Action { get; } public string Action { get; }
public Session Game { get; set; } public SessionMetadata Game { get; set; }
/// <summary> /// <summary>
/// The player who created the game. /// The player who created the game.

View File

@@ -1,6 +1,6 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{ {
public interface IResponse public interface ISocketResponse
{ {
string Action { get; } string Action { get; }
} }

View File

@@ -14,7 +14,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
public string GameName { get; set; } = ""; public string GameName { get; set; } = "";
} }
public class JoinGameResponse : IResponse public class JoinGameResponse : ISocketResponse
{ {
public string Action { get; protected set; } public string Action { get; protected set; }
public string GameName { get; set; } public string GameName { get; set; }
@@ -31,7 +31,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
} }
} }
public class JoinByCodeResponse : JoinGameResponse, IResponse public class JoinByCodeResponse : JoinGameResponse, ISocketResponse
{ {
public JoinByCodeResponse() public JoinByCodeResponse()
{ {

View File

@@ -2,7 +2,7 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
{ {
public class MoveResponse : IResponse public class MoveResponse : ISocketResponse
{ {
public string Action { get; } public string Action { get; }
public string GameName { get; set; } public string GameName { get; set; }

View File

@@ -0,0 +1,12 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
{
public class SessionMetadata
{
public string Name { get; set; }
public string Player1 { get; set; }
public string? Player2 { get; set; }
public bool IsPrivate { get; set; }
public string[] Players => string.IsNullOrEmpty(Player2) ? new[] { Player1 } : new[] { Player1, Player2 };
}
}

View File

@@ -9,16 +9,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.S
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.AcceptanceTests", "Shogi.AcceptanceTests\Shogi.AcceptanceTests.csproj", "{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -33,18 +31,10 @@ Global
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.Build.0 = Release|Any CPU
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.Build.0 = Release|Any CPU
{0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -53,14 +43,17 @@ Global
{F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.Build.0 = Release|Any CPU {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.Build.0 = Release|Any CPU
{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
{12530716-C11E-40CE-9F71-CCCC243F03E1} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
{F256989E-B6AF-4731-9DB4-88991C40B2CE} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} {F256989E-B6AF-4731-9DB4-88991C40B2CE} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}

View File

@@ -21,15 +21,18 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
private readonly IGameboardManager gameboardManager; private readonly IGameboardManager gameboardManager;
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardRepository gameboardRepository;
private readonly ISocketConnectionManager communicationManager; private readonly ISocketConnectionManager communicationManager;
private readonly IModelMapper mapper;
public GameController( public GameController(
IGameboardRepository repository, IGameboardRepository repository,
IGameboardManager manager, IGameboardManager manager,
ISocketConnectionManager communicationManager) ISocketConnectionManager communicationManager,
IModelMapper mapper)
{ {
gameboardManager = manager; gameboardManager = manager;
gameboardRepository = repository; gameboardRepository = repository;
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
this.mapper = mapper;
} }
[HttpPost("JoinCode")] [HttpPost("JoinCode")]
@@ -75,32 +78,35 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
{ {
return NotFound(); return NotFound();
} }
if (user == null || (session.Player1.Id != user.Id && session.Player2?.Id != user.Id)) if (user == null || (session.Player1Name != user.Id && session.Player2Name != user.Id))
{ {
return Forbid("User is not seated at this game."); return Forbid("User is not seated at this game.");
} }
var move = request.Move; try
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)
{ {
var createSuccess = await gameboardRepository.CreateBoardState(session); var move = request.Move;
if (!createSuccess) if (move.PieceFromCaptured.HasValue)
{ session.Move(mapper.Map(move.PieceFromCaptured.Value), move.To);
throw new ApplicationException("Unable to persist board state."); else if (!string.IsNullOrWhiteSpace(move.From))
} session.Move(move.From, move.To, move.IsPromotion);
await communicationManager.BroadcastToPlayers(new MoveResponse
{ await gameboardRepository.CreateBoardState(session);
GameName = session.Name, await communicationManager.BroadcastToPlayers(
PlayerName = user.Id new MoveResponse
}, session.Player1.Id, session.Player2?.Id); {
GameName = session.Name,
PlayerName = user.Id
},
session.Player1Name,
session.Player2Name);
return Ok(); return Ok();
} }
return Conflict("Illegal move."); catch (InvalidOperationException ex)
{
return Conflict(ex.Message);
}
} }
// TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too.
@@ -128,28 +134,15 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
public async Task<IActionResult> PostSession([FromBody] PostSession request) public async Task<IActionResult> PostSession([FromBody] PostSession request)
{ {
var user = await ReadUserOrThrow(); var user = await ReadUserOrThrow();
var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!); var session = new Shogi.Domain.SessionMetadata(request.Name, request.IsPrivate, user.Id);
var success = await gameboardRepository.CreateSession(session); await gameboardRepository.CreateSession(session);
await communicationManager.BroadcastToAll(new CreateGameResponse
if (success)
{ {
try Game = mapper.Map(session),
{ PlayerName = user.Id
});
await communicationManager.BroadcastToAll(new CreateGameResponse return Ok();
{
Game = session.ToServiceModel(),
PlayerName = user.Id
});
}
catch (Exception e)
{
Console.Error.WriteLine("Error broadcasting during PostSession");
}
return Ok();
}
return Conflict();
} }
@@ -174,40 +167,16 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
{ {
BoardState = new BoardState BoardState = new BoardState
{ {
Board = null, Board = mapper.Map(session.BoardState),
Player1Hand = session.Player1Hand.Select(MapPiece).ToList(), Player1Hand = session.Player1Hand.Select(mapper.Map).ToList(),
Player2Hand = session.Player2Hand.Select(MapPiece).ToList(), Player2Hand = session.Player2Hand.Select(mapper.Map).ToList(),
PlayerInCheck = session.InCheck.HasValue ? Map(session.InCheck.Value) : null PlayerInCheck = mapper.Map(session.InCheck)
}, },
GameName = session.Name, GameName = session.Name,
Player1 = session.Player1Name, Player1 = session.Player1Name,
Player2 = session.Player2Name Player2 = session.Player2Name
}; };
return this.Ok(response); return Ok(response);
static WhichPlayer Map(Shogi.Domain.WhichPlayer whichPlayer)
{
return whichPlayer == Shogi.Domain.WhichPlayer.Player1
? WhichPlayer.Player1
: WhichPlayer.Player2;
}
static Piece MapPiece(Shogi.Domain.Pieces.Piece piece)
{
var owner = Map(piece.Owner);
var whichPiece = piece.WhichPiece switch
{
Shogi.Domain.WhichPiece.King => WhichPiece.King,
Shogi.Domain.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral,
Shogi.Domain.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral,
Shogi.Domain.WhichPiece.Bishop => WhichPiece.Bishop,
Shogi.Domain.WhichPiece.Rook => WhichPiece.Rook,
Shogi.Domain.WhichPiece.Knight => WhichPiece.Knight,
Shogi.Domain.WhichPiece.Lance => WhichPiece.Lance,
Shogi.Domain.WhichPiece.Pawn => WhichPiece.Pawn,
_ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}")
};
return new Piece { IsPromoted = piece.IsPromoted, Owner = owner, WhichPiece = whichPiece };
}
} }
[HttpGet] [HttpGet]
@@ -217,18 +186,18 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
var sessions = await gameboardRepository.ReadSessionMetadatas(); var sessions = await gameboardRepository.ReadSessionMetadatas();
var sessionsJoinedByUser = sessions var sessionsJoinedByUser = sessions
.Where(s => s.IsSeated(user)) .Where(s => s.IsSeated(user.Id))
.Select(s => s.ToServiceModel()) .Select(s => mapper.Map(s))
.ToList(); .ToList();
var sessionsNotJoinedByUser = sessions var sessionsNotJoinedByUser = sessions
.Where(s => !s.IsSeated(user)) .Where(s => !s.IsSeated(user.Id))
.Select(s => s.ToServiceModel()) .Select(s => mapper.Map(s))
.ToList(); .ToList();
return new GetSessionsResponse return new GetSessionsResponse
{ {
PlayerHasJoinedSessions = new Collection<Session>(sessionsJoinedByUser), PlayerHasJoinedSessions = sessionsJoinedByUser,
AllOtherSessions = new Collection<Session>(sessionsNotJoinedByUser) AllOtherSessions = sessionsNotJoinedByUser
}; };
} }
@@ -246,13 +215,12 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
return this.Conflict("This session already has two seated players and is full."); return this.Conflict("This session already has two seated players and is full.");
} }
session.SetPlayer2(user); session.SetPlayer2(user.Id);
var success = await gameboardRepository.UpdateSession(session); await gameboardRepository.UpdateSession(session);
if (!success) return this.Problem(detail: "Unable to update session.");
var opponentName = user.Id == session.Player1.Id var opponentName = user.Id == session.Player1
? session.Player2!.Id ? session.Player2!
: session.Player1.Id; : session.Player1;
await communicationManager.BroadcastToPlayers(new JoinGameResponse await communicationManager.BroadcastToPlayers(new JoinGameResponse
{ {
GameName = session.Name, GameName = session.Name,

View File

@@ -68,10 +68,8 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
var user = await gameboardManager.ReadUser(User); var user = await gameboardManager.ReadUser(User);
if (user == null) if (user == null)
{ {
if (await gameboardManager.CreateUser(User)) await gameboardManager.CreateUser(User);
{ user = await gameboardManager.ReadUser(User);
user = await gameboardManager.ReadUser(User);
}
} }
if (user == null) if (user == null)
@@ -92,11 +90,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
{ {
// Create a guest user. // Create a guest user.
var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString()); var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString());
var success = await gameboardRepository.CreateUser(newUser); await gameboardRepository.CreateUser(newUser);
if (!success)
{
return Conflict();
}
var identity = newUser.CreateClaimsIdentity(); var identity = newUser.CreateClaimsIdentity();
await HttpContext.SignInAsync( await HttpContext.SignInAsync(

View File

@@ -7,68 +7,68 @@ using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers namespace Gameboard.ShogiUI.Sockets.Managers
{ {
public interface IGameboardManager public interface IGameboardManager
{ {
Task<bool> AssignPlayer2ToSession(string sessionName, User user); Task AssignPlayer2ToSession(string sessionName, User user);
Task<User?> ReadUser(ClaimsPrincipal user); Task<User?> ReadUser(ClaimsPrincipal user);
Task<bool> CreateUser(ClaimsPrincipal user); Task<User?> CreateUser(ClaimsPrincipal user);
} }
public class GameboardManager : IGameboardManager public class GameboardManager : IGameboardManager
{ {
private readonly IGameboardRepository repository; private readonly IGameboardRepository repository;
public GameboardManager(IGameboardRepository repository) public GameboardManager(IGameboardRepository repository)
{ {
this.repository = repository; this.repository = repository;
} }
public Task<bool> CreateUser(ClaimsPrincipal principal) public async Task<User> CreateUser(ClaimsPrincipal principal)
{ {
var id = principal.UserId(); var id = principal.UserId();
if (string.IsNullOrEmpty(id)) if (string.IsNullOrEmpty(id))
{ {
return Task.FromResult(false); throw new InvalidOperationException("Cannot create user from given claims.");
} }
var user = principal.IsGuest() var user = principal.IsGuest()
? User.CreateGuestUser(id) ? User.CreateGuestUser(id)
: User.CreateMsalUser(id); : User.CreateMsalUser(id);
return repository.CreateUser(user); await repository.CreateUser(user);
} return user;
}
public Task<User?> ReadUser(ClaimsPrincipal principal) public Task<User?> ReadUser(ClaimsPrincipal principal)
{ {
var userId = principal.UserId(); var userId = principal.UserId();
if (!string.IsNullOrEmpty(userId)) if (!string.IsNullOrEmpty(userId))
{ {
return repository.ReadUser(userId); return repository.ReadUser(userId);
} }
return Task.FromResult<User?>(null); return Task.FromResult<User?>(null);
} }
public async Task<string> CreateJoinCode(string sessionName, string playerName) public async Task<string> CreateJoinCode(string sessionName, string playerName)
{ {
//var session = await repository.GetGame(sessionName); //var session = await repository.GetGame(sessionName);
//if (playerName == session?.Player1) //if (playerName == session?.Player1)
//{ //{
// return await repository.PostJoinCode(sessionName, playerName); // return await repository.PostJoinCode(sessionName, playerName);
//} //}
return string.Empty; return string.Empty;
} }
public async Task<bool> AssignPlayer2ToSession(string sessionName, User user) public async Task AssignPlayer2ToSession(string sessionName, User user)
{ {
var session = await repository.ReadSessionMetaData(sessionName); var session = await repository.ReadSessionMetaData(sessionName);
if (session != null && !session.IsPrivate && session.Player2 == null) if (session != null && !session.IsPrivate && session.Player2 == null)
{ {
session.SetPlayer2(user); session.SetPlayer2(user.Id);
return await repository.UpdateSession(session); await repository.UpdateSession(session);
} }
return false; }
} }
}
} }

View File

@@ -0,0 +1,100 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using DomainWhichPiece = Shogi.Domain.WhichPiece;
using DomainWhichPlayer = Shogi.Domain.WhichPlayer;
namespace Gameboard.ShogiUI.Sockets.Managers
{
public class ModelMapper : IModelMapper
{
public WhichPlayer Map(DomainWhichPlayer whichPlayer)
{
return whichPlayer switch
{
DomainWhichPlayer.Player1 => WhichPlayer.Player1,
DomainWhichPlayer.Player2 => WhichPlayer.Player2,
_ => throw new ArgumentException("Unrecognized value for WhichPlayer", nameof(whichPlayer))
};
}
public WhichPlayer? Map(DomainWhichPlayer? whichPlayer)
{
return whichPlayer.HasValue
? Map(whichPlayer.Value)
: null;
}
public WhichPiece Map(DomainWhichPiece whichPiece)
{
return whichPiece switch
{
DomainWhichPiece.King => WhichPiece.King,
DomainWhichPiece.GoldGeneral => WhichPiece.GoldGeneral,
DomainWhichPiece.SilverGeneral => WhichPiece.SilverGeneral,
DomainWhichPiece.Bishop => WhichPiece.Bishop,
DomainWhichPiece.Rook => WhichPiece.Rook,
DomainWhichPiece.Knight => WhichPiece.Knight,
DomainWhichPiece.Lance => WhichPiece.Lance,
DomainWhichPiece.Pawn => WhichPiece.Pawn,
_ => throw new ArgumentException("Unrecognized value", nameof(whichPiece)),
};
}
public DomainWhichPiece Map(WhichPiece whichPiece)
{
return whichPiece switch
{
WhichPiece.King => DomainWhichPiece.King,
WhichPiece.GoldGeneral => DomainWhichPiece.GoldGeneral,
WhichPiece.SilverGeneral => DomainWhichPiece.SilverGeneral,
WhichPiece.Bishop => DomainWhichPiece.Bishop,
WhichPiece.Rook => DomainWhichPiece.Rook,
WhichPiece.Knight => DomainWhichPiece.Knight,
WhichPiece.Lance => DomainWhichPiece.Lance,
WhichPiece.Pawn => DomainWhichPiece.Pawn,
_ => throw new ArgumentException("Unrecognized value", nameof(whichPiece)),
};
}
public SessionMetadata Map(Shogi.Domain.SessionMetadata session)
{
return new SessionMetadata
{
Name = session.Name,
Player1 = session.Player1,
Player2 = session.Player2,
IsPrivate = session.IsPrivate
};
}
public Piece Map(Shogi.Domain.Pieces.Piece piece)
{
return new Piece { IsPromoted = piece.IsPromoted, Owner = Map(piece.Owner), WhichPiece = Map(piece.WhichPiece) };
}
public Dictionary<string, Piece?> Map(IDictionary<string, Shogi.Domain.Pieces.Piece?> boardState)
{
return boardState.ToDictionary(kvp => kvp.Key, kvp => MapNullable(kvp.Value));
}
public Piece? MapNullable(Shogi.Domain.Pieces.Piece? piece)
{
if (piece == null) return null;
return Map(piece);
}
}
public interface IModelMapper
{
WhichPlayer Map(DomainWhichPlayer whichPlayer);
WhichPlayer? Map(DomainWhichPlayer? whichPlayer);
WhichPiece Map(DomainWhichPiece whichPiece);
DomainWhichPiece Map(WhichPiece value);
SessionMetadata Map(Shogi.Domain.SessionMetadata session);
Piece Map(Shogi.Domain.Pieces.Piece p);
Piece? MapNullable(Shogi.Domain.Pieces.Piece? p);
Dictionary<string, Piece?> Map(IDictionary<string, Shogi.Domain.Pieces.Piece?> boardState);
}
}

View File

@@ -12,10 +12,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers
{ {
public interface ISocketConnectionManager public interface ISocketConnectionManager
{ {
Task BroadcastToAll(IResponse response); Task BroadcastToAll(ISocketResponse response);
void Subscribe(WebSocket socket, string playerName); void Subscribe(WebSocket socket, string playerName);
void Unsubscribe(string playerName); void Unsubscribe(string playerName);
Task BroadcastToPlayers(IResponse response, params string?[] playerNames); Task BroadcastToPlayers(ISocketResponse response, params string?[] playerNames);
} }
/// <summary> /// <summary>
@@ -45,7 +45,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
connections.TryRemove(playerName, out _); connections.TryRemove(playerName, out _);
} }
public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames) public async Task BroadcastToPlayers(ISocketResponse response, params string?[] playerNames)
{ {
var tasks = new List<Task>(playerNames.Length); var tasks = new List<Task>(playerNames.Length);
foreach (var name in playerNames) foreach (var name in playerNames)
@@ -59,7 +59,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
} }
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
} }
public Task BroadcastToAll(IResponse response) public Task BroadcastToAll(ISocketResponse response)
{ {
var message = JsonConvert.SerializeObject(response); var message = JsonConvert.SerializeObject(response);
logger.LogInformation($"Broadcasting\n{0}", message); logger.LogInformation($"Broadcasting\n{0}", message);

View File

@@ -1,63 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Gameboard.ShogiUI.Sockets.Utilities;
using System.Diagnostics;
using System.Numerics;
namespace Gameboard.ShogiUI.Sockets.Models
{
[DebuggerDisplay("{From} - {To}")]
public class Move
{
public Vector2? From { get; } // TODO: Use string notation
public bool IsPromotion { get; }
public WhichPiece? PieceFromHand { get; }
public Vector2 To { get; }
public Move(Vector2 from, Vector2 to, bool isPromotion = false)
{
From = from;
To = to;
IsPromotion = isPromotion;
}
public Move(WhichPiece pieceFromHand, Vector2 to)
{
PieceFromHand = pieceFromHand;
To = to;
}
/// <summary>
/// Constructor to represent moving a piece on the Board to another position on the Board.
/// </summary>
/// <param name="fromNotation">Position the piece is being moved from.</param>
/// <param name="toNotation">Position the piece is being moved to.</param>
/// <param name="isPromotion">If the moving piece should be promoted.</param>
public Move(string fromNotation, string toNotation, bool isPromotion = false)
{
From = NotationHelper.FromBoardNotation(fromNotation);
To = NotationHelper.FromBoardNotation(toNotation);
IsPromotion = isPromotion;
}
/// <summary>
/// Constructor to represent moving a piece from the Hand to the Board.
/// </summary>
/// <param name="pieceFromHand">The piece being moved from the Hand to the Board.</param>
/// <param name="toNotation">Position the piece is being moved to.</param>
/// <param name="isPromotion">If the moving piece should be promoted.</param>
public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false)
{
From = null;
PieceFromHand = pieceFromHand;
To = NotationHelper.FromBoardNotation(toNotation);
IsPromotion = isPromotion;
}
public ServiceModels.Types.Move ToServiceModel() => new()
{
From = From.HasValue ? NotationHelper.ToBoardNotation(From.Value) : null,
IsPromotion = IsPromotion,
PieceFromCaptured = PieceFromHand.HasValue ? PieceFromHand : null,
To = NotationHelper.ToBoardNotation(To)
};
}
}

View File

@@ -1,37 +0,0 @@
namespace Gameboard.ShogiUI.Sockets.Models
{
/// <summary>
/// A representation of a Session without the board and game-rules.
/// </summary>
public class SessionMetadata
{
public string Name { get; }
public User Player1 { get; }
public User? Player2 { get; private set; }
public bool IsPrivate { get; }
public SessionMetadata(string name, bool isPrivate, User player1, User? player2 = null)
{
Name = name;
IsPrivate = isPrivate;
Player1 = player1;
Player2 = player2;
}
public SessionMetadata(Session sessionModel)
{
Name = sessionModel.Name;
IsPrivate = sessionModel.IsPrivate;
Player1 = sessionModel.Player1;
Player2 = sessionModel.Player2;
}
public void SetPlayer2(User user)
{
Player2 = user;
}
public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id;
public ServiceModels.Types.Session ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
}
}

View File

@@ -1,4 +1,15 @@
{ {
"profiles": {
"Kestrel": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "/swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
},
"$schema": "http://json.schemastore.org/launchsettings.json", "$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": { "iisSettings": {
"windowsAuthentication": false, "windowsAuthentication": false,
@@ -7,23 +18,5 @@
"applicationUrl": "http://localhost:50728/", "applicationUrl": "http://localhost:50728/",
"sslPort": 44315 "sslPort": 44315
} }
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Kestrel": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "/swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5100"
}
} }
} }

View File

@@ -1,4 +1,6 @@
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels using Shogi.Domain;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{ {
public class SessionDocument : CouchDocument public class SessionDocument : CouchDocument
{ {
@@ -17,12 +19,12 @@
Player2Id = string.Empty; Player2Id = string.Empty;
} }
public SessionDocument(Models.SessionMetadata sessionMetaData) public SessionDocument(SessionMetadata sessionMetaData)
: base(sessionMetaData.Name, WhichDocumentType.Session) : base(sessionMetaData.Name, WhichDocumentType.Session)
{ {
Name = sessionMetaData.Name; Name = sessionMetaData.Name;
Player1Id = sessionMetaData.Player1.Id; Player1Id = sessionMetaData.Player1;
Player2Id = sessionMetaData.Player2?.Id; Player2Id = sessionMetaData.Player2;
IsPrivate = sessionMetaData.IsPrivate; IsPrivate = sessionMetaData.IsPrivate;
} }
} }

View File

@@ -17,13 +17,13 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
{ {
public interface IGameboardRepository public interface IGameboardRepository
{ {
Task<bool> CreateBoardState(Session session); Task CreateBoardState(Session session);
Task<bool> CreateSession(Models.SessionMetadata session); Task CreateSession(SessionMetadata session);
Task<bool> CreateUser(Models.User user); Task CreateUser(Models.User user);
Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas(); Task<Collection<SessionMetadata>> ReadSessionMetadatas();
Task<Session?> ReadSession(string name); Task<Session?> ReadSession(string name);
Task<bool> UpdateSession(Models.SessionMetadata session); Task UpdateSession(SessionMetadata session);
Task<Models.SessionMetadata?> ReadSessionMetaData(string name); Task<SessionMetadata?> ReadSessionMetaData(string name);
Task<Models.User?> ReadUser(string userName); Task<Models.User?> ReadUser(string userName);
} }
@@ -48,7 +48,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
this.logger = logger; this.logger = logger;
} }
public async Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas() public async Task<Collection<SessionMetadata>> ReadSessionMetadatas()
{ {
var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString(); var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
@@ -57,7 +57,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
if (result != null) if (result != null)
{ {
var groupedBySession = result.rows.GroupBy(row => row.id); var groupedBySession = result.rows.GroupBy(row => row.id);
var sessions = new List<Models.SessionMetadata>(result.total_rows / 3); var sessions = new List<SessionMetadata>(result.total_rows / 3);
foreach (var group in groupedBySession) foreach (var group in groupedBySession)
{ {
/** /**
@@ -72,12 +72,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
if (session != null && player1Doc != null) if (session != null && player1Doc != null)
{ {
var player2 = player2Doc == null ? null : new Models.User(player2Doc); var player2 = player2Doc == null ? null : new Models.User(player2Doc);
sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2)); sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1Doc.Id, player2?.Id));
} }
} }
return new Collection<Models.SessionMetadata>(sessions); return new Collection<SessionMetadata>(sessions);
} }
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>()); return new Collection<SessionMetadata>(Array.Empty<SessionMetadata>());
} }
public async Task<Session?> ReadSession(string name) public async Task<Session?> ReadSession(string name)
@@ -130,7 +130,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return null; return null;
} }
public async Task<Models.SessionMetadata?> ReadSessionMetaData(string name) public async Task<SessionMetadata?> ReadSessionMetaData(string name)
{ {
var queryParams = new QueryBuilder var queryParams = new QueryBuilder
{ {
@@ -159,7 +159,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
if (session != null && player1Doc != null) if (session != null && player1Doc != null)
{ {
var player2 = player2Doc == null ? null : new Models.User(player2Doc); var player2 = player2Doc == null ? null : new Models.User(player2Doc);
return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2); return new SessionMetadata(session.Name, session.IsPrivate, player1Doc.Id, player2?.Id);
} }
} }
return null; return null;
@@ -168,22 +168,15 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
/// <summary> /// <summary>
/// Saves a snapshot of board state and the most recent move. /// Saves a snapshot of board state and the most recent move.
/// </summary> /// </summary>
public async Task<bool> CreateBoardState(Session session) public async Task CreateBoardState(Session session)
{ {
Piece? MapPiece(Shogi.Domain.Pieces.Piece? piece)
{
return piece == null
? null
: new Piece { IsPromoted = piece.IsPromoted, Owner = piece.Owner, WhichPiece = piece.WhichPiece };
}
var boardStateDocument = new BoardStateDocument(session.Name, session); var boardStateDocument = new BoardStateDocument(session.Name, session);
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content); var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode; response.EnsureSuccessStatusCode();
} }
public async Task<bool> CreateSession(Models.SessionMetadata session) public async Task CreateSession(SessionMetadata session)
{ {
var sessionDocument = new SessionDocument(session); var sessionDocument = new SessionDocument(session);
var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson); var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
@@ -195,16 +188,15 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
if ((await postSessionDocumentTask).IsSuccessStatusCode) if ((await postSessionDocumentTask).IsSuccessStatusCode)
{ {
var response = await client.PostAsync(string.Empty, boardStateContent); var response = await client.PostAsync(string.Empty, boardStateContent);
return response.IsSuccessStatusCode; response.EnsureSuccessStatusCode();
} }
return false;
} }
public async Task<bool> UpdateSession(Models.SessionMetadata session) public async Task UpdateSession(SessionMetadata session)
{ {
// GET existing session to get revisionId. // GET existing session to get revisionId.
var readResponse = await client.GetAsync(session.Name); var readResponse = await client.GetAsync(session.Name);
if (!readResponse.IsSuccessStatusCode) return false; readResponse.EnsureSuccessStatusCode();
var sessionDocument = JsonConvert.DeserializeObject<SessionDocument>(await readResponse.Content.ReadAsStringAsync()); var sessionDocument = JsonConvert.DeserializeObject<SessionDocument>(await readResponse.Content.ReadAsStringAsync());
// PUT the document with the revisionId. // PUT the document with the revisionId.
@@ -214,7 +206,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
}; };
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PutAsync(couchModel.Id, content); var response = await client.PutAsync(couchModel.Id, content);
return response.IsSuccessStatusCode; response.EnsureSuccessStatusCode();
} }
//public async Task<bool> PutJoinPublicSession(PutJoinPublicSession request) //public async Task<bool> PutJoinPublicSession(PutJoinPublicSession request)
//{ //{
@@ -285,12 +277,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
return null; return null;
} }
public async Task<bool> CreateUser(Models.User user) public async Task CreateUser(Models.User user)
{ {
var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform); var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content); var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode; response.EnsureSuccessStatusCode();
} }
public void ReadMoveHistory() public void ReadMoveHistory()

View File

@@ -28,8 +28,8 @@ namespace Gameboard.ShogiUI.Sockets
if (user == null) if (user == null)
{ {
var newUser = Models.User.CreateMsalUser(nameClaim.Value); var newUser = Models.User.CreateMsalUser(nameClaim.Value);
var success = await gameboardRepository.CreateUser(newUser); await gameboardRepository.CreateUser(newUser);
if (success) user = newUser; user = newUser;
} }
if (user != null) if (user != null)

View File

@@ -58,6 +58,7 @@ namespace Gameboard.ShogiUI.Sockets
var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/"; var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/";
c.BaseAddress = new Uri(baseUrl); c.BaseAddress = new Uri(baseUrl);
}); });
services.AddTransient<IModelMapper, ModelMapper>();
services services
.AddControllers() .AddControllers()
@@ -128,13 +129,17 @@ namespace Gameboard.ShogiUI.Sockets
}; };
}); });
// Remove default HttpClient logging. services.AddHttpLogging(options =>
services.RemoveAll<IHttpMessageHandlerBuilderFilter>(); {
options.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.Request;
});
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager) public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager)
{ {
app.UseHttpLogging();
var origins = new[] { var origins = new[] {
"http://localhost:3000", "https://localhost:3000", "http://localhost:3000", "https://localhost:3000",
"http://127.0.0.1:3000", "https://127.0.0.1:3000", "http://127.0.0.1:3000", "https://127.0.0.1:3000",
@@ -147,25 +152,25 @@ namespace Gameboard.ShogiUI.Sockets
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
var client = PublicClientApplicationBuilder //var client = PublicClientApplicationBuilder
.Create(Configuration["AzureAd:ClientId"]) // .Create(Configuration["AzureAd:ClientId"])
.WithLogging( // .WithLogging(
(level, message, pii) => // (level, message, pii) =>
{ // {
Console.WriteLine(message); // Console.WriteLine(message);
}, // },
LogLevel.Verbose, // LogLevel.Verbose,
true, // true,
true // true
) // )
.Build(); // .Build();
} }
else else
{ {
app.UseHsts(); app.UseHsts();
} }
app app
.UseRequestResponseLogging() //.UseRequestResponseLogging()
.UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials()) .UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials())
.UseRouting() .UseRouting()
.UseAuthentication() .UseAuthentication()

View File

@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="xunit" Version="2.4.2-pre.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,36 +0,0 @@
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PathFinding;
using System.Numerics;
namespace Gameboard.ShogiUI.UnitTests.PathFinding
{
[TestClass]
public class PathFinder2DShould
{
[TestMethod]
public void Maths()
{
var result = PathFinder2D<IPlanarElement>.IsPathable(
new Vector2(2, 2),
new Vector2(7, 7),
new Vector2(1, 1)
);
result.Should().BeTrue();
result = PathFinder2D<IPlanarElement>.IsPathable(
new Vector2(2, 2),
new Vector2(7, 7),
new Vector2(0, 0)
);
result.Should().BeFalse();
result = PathFinder2D<IPlanarElement>.IsPathable(
new Vector2(2, 2),
new Vector2(7, 7),
new Vector2(-1, 1)
);
result.Should().BeFalse();
}
}
}

View File

@@ -1,57 +0,0 @@
using AutoFixture;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.UnitTests.PathFinding
{
[TestClass]
public class PlanarCollectionShould
{
[TestMethod]
public void Index()
{
// Arrange
var collection = new TestPlanarCollection();
var expected1 = new SimpleElement(1);
var expected2 = new SimpleElement(2);
// Act
collection[0, 0] = expected1;
collection[2, 1] = expected2;
// Assert
collection[0, 0].Should().Be(expected1);
collection[2, 1].Should().Be(expected2);
}
[TestMethod]
public void Iterate()
{
// Arrange
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 planarCollection)
actual.Add(elem);
// Assert
using (new AssertionScope())
{
actual[0].Number.Should().Be(1);
actual[1].Number.Should().Be(2);
actual[2].Number.Should().Be(3);
actual[3].Number.Should().Be(4);
actual[4].Number.Should().Be(5);
}
}
}
}

View File

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

View File

@@ -1,15 +0,0 @@
using FluentAssertions;
using Gameboard.ShogiUI.Sockets.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Numerics;
using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer;
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece;
namespace Gameboard.ShogiUI.UnitTests.Rules
{
[TestClass]
public class ShogiBoardShould
{
}
}

View File

@@ -1,17 +0,0 @@
using FluentAssertions;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Xunit;
namespace Gameboard.ShogiUI.xUnitTests
{
public class GameShould
{
[Fact]
public void DiscardNullPLayers()
{
var game = new Session("Test", "P1", null);
game.Players.Count.Should().Be(1);
}
}
}

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
using System.Numerics;
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 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);
}
}

View File

@@ -1,8 +0,0 @@
namespace PathFinding
{
public enum Distance
{
OneStep,
MultiStep
}
}

View File

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

View File

@@ -1,9 +0,0 @@
namespace PathFinding
{
public interface IPlanarElement
{
MoveSet MoveSet { get; }
bool IsUpsideDown { get; }
}
}

View File

@@ -1,17 +0,0 @@
using System.Diagnostics;
using System.Numerics;
namespace PathFinding
{
[DebuggerDisplay("{Direction} - {Distance}")]
public class Move
{
public Vector2 Direction { get; }
public Distance Distance { get; }
public Move(Vector2 direction, Distance distance = Distance.OneStep)
{
Direction = direction;
Distance = distance;
}
}
}

View File

@@ -1,23 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace PathFinding
{
public class MoveSet
{
private readonly IPlanarElement element;
private readonly ICollection<Move> moves;
private readonly ICollection<Move> upsidedownMoves;
public MoveSet(IPlanarElement element, ICollection<Move> moves)
{
this.element = element;
this.moves = moves;
upsidedownMoves = moves.Select(_ => new Move(Vector2.Negate(_.Direction), _.Distance)).ToList();
}
public ICollection<Move> GetMoves() => element.IsUpsideDown ? upsidedownMoves : moves;
}
}

View File

@@ -1,141 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace PathFinding
{
public class PathFinder2D<T> where T : IPlanarElement
{
/// <param name="element">Guaranteed to be non-null.</param>
/// <param name="position"></param>
public delegate void Callback(T collider, Vector2 position);
private readonly IPlanarCollection<T> collection;
private readonly int width;
private readonly int height;
/// <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;
this.width = width;
this.height = height;
}
/// <summary>
/// Navigate the collection such that each "step" is always towards the destination, respecting the Paths available to the element at origin.
/// </summary>
/// <param name="element">The pathing element.</param>
/// <param name="origin">The starting location.</param>
/// <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)
{
if (destination.X > width - 1 || destination.Y > height - 1 || destination.X < 0 || destination.Y < 0)
{
return false;
}
var element = collection[origin];
if (element == null) return false;
var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination);
if (!IsPathable(origin, destination, path.Direction))
{
// Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen.
return false;
}
var shouldPath = true;
var next = origin;
while (shouldPath && next != destination)
{
next = Vector2.Add(next, path.Direction);
var collider = collection[next];
if (collider != null)
{
callback?.Invoke(collider, next);
shouldPath = false;
}
else if (path.Distance == Distance.OneStep)
{
shouldPath = false;
}
}
return next == destination;
}
public void PathEvery(Vector2 from, Callback callback)
{
var element = collection[from];
if (element == null)
{
return;
}
foreach (var path in element.MoveSet.GetMoves())
{
var shouldPath = true;
var next = Vector2.Add(from, path.Direction); ;
while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0)
{
var collider = collection[(int)next.Y, (int)next.X];
if (collider != null)
{
callback(collider, next);
shouldPath = false;
}
if (path.Distance == Distance.OneStep)
{
shouldPath = false;
}
next = Vector2.Add(next, path.Direction);
}
}
}
/// <summary>
/// Path the line from origin to destination, ignoring any Paths defined by the element at origin.
/// </summary>
public void LinePathTo(Vector2 origin, Vector2 direction, Callback callback)
{
direction = Vector2.Normalize(direction);
var next = Vector2.Add(origin, direction);
while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height)
{
var element = collection[next];
if (element != null) callback(element, next);
next = Vector2.Add(next, direction);
}
}
public static Move FindDirectionTowardsDestination(ICollection<Move> paths, Vector2 origin, Vector2 destination) =>
paths.Aggregate((a, b) =>
{
var distanceA = Vector2.Distance(destination, Vector2.Add(origin, a.Direction));
var distanceB = Vector2.Distance(destination, Vector2.Add(origin, b.Direction));
return distanceA < distanceB ? a : b;
});
public static bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction)
{
var next = Vector2.Add(origin, direction);
if (Vector2.Distance(next, destination) >= Vector2.Distance(origin, destination)) return false;
var slope = (destination.Y - origin.Y) / (destination.X - origin.X);
if (float.IsInfinity(slope))
{
return next.X == destination.X;
}
else
{
// b = -mx + y
var yIntercept = -slope * origin.X + origin.Y;
// y = mx + b
return next.Y == slope * next.X + yIntercept;
}
}
}
}

View File

@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
namespace Shogi.AcceptanceTests
{
public class AcceptanceTests
{
public AcceptanceTests()
{
}
[Fact]
public void CreateAndReadSession()
{
}
}
}

View File

@@ -2,33 +2,23 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="FluentAssertions" Version="6.2.0" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02" />
<PackageReference Include="xunit" Version="2.4.2-pre.12" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0"> <PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -22,5 +22,7 @@
{ {
Player2 = user; Player2 = user;
} }
public bool IsSeated(string playerName) => playerName == Player1 || playerName == Player2;
} }
} }