squash a bunch of commits
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -53,3 +53,5 @@ Thumbs.db
|
|||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
*.user
|
*.user
|
||||||
|
/Shogi.Database/Shogi.Database.dbmdl
|
||||||
|
/Shogi.Database/Shogi.Database.jfm
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|
||||||
{
|
|
||||||
public class GetGuestTokenResponse
|
|
||||||
{
|
|
||||||
public string UserId { get; }
|
|
||||||
public string DisplayName { get; }
|
|
||||||
public Guid OneTimeToken { get; }
|
|
||||||
|
|
||||||
public GetGuestTokenResponse(string id, string displayName, Guid token)
|
|
||||||
{
|
|
||||||
UserId = id;
|
|
||||||
DisplayName = displayName;
|
|
||||||
OneTimeToken = token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|
||||||
{
|
|
||||||
public class GetSessionsResponse
|
|
||||||
{
|
|
||||||
public IList<SessionMetadata> PlayerHasJoinedSessions { get; set; }
|
|
||||||
public IList<SessionMetadata> AllOtherSessions { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|
||||||
{
|
|
||||||
public class GetTokenResponse
|
|
||||||
{
|
|
||||||
public Guid OneTimeToken { get; }
|
|
||||||
|
|
||||||
public GetTokenResponse(Guid token)
|
|
||||||
{
|
|
||||||
OneTimeToken = token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|
||||||
{
|
|
||||||
public class PostGameInvitation
|
|
||||||
{
|
|
||||||
public string SessionName { get; set; }
|
|
||||||
}
|
|
||||||
public class PostGuestGameInvitation
|
|
||||||
{
|
|
||||||
public string GuestId { get; set; }
|
|
||||||
public string SessionName { get; set; }
|
|
||||||
}
|
|
||||||
public class PostGameInvitationResponse
|
|
||||||
{
|
|
||||||
public string Code { get; }
|
|
||||||
public PostGameInvitationResponse(string code)
|
|
||||||
{
|
|
||||||
Code = code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|
||||||
{
|
|
||||||
public class PostMove
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public Move Move { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|
||||||
{
|
|
||||||
public class PostSession
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public bool IsPrivate { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|
||||||
{
|
|
||||||
public class CreateGameResponse : ISocketResponse
|
|
||||||
{
|
|
||||||
public string Action { get; }
|
|
||||||
public SessionMetadata Game { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The player who created the game.
|
|
||||||
/// </summary>
|
|
||||||
public string PlayerName { get; set; }
|
|
||||||
|
|
||||||
public CreateGameResponse()
|
|
||||||
{
|
|
||||||
Action = ClientAction.CreateGame.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|
||||||
{
|
|
||||||
public interface IRequest
|
|
||||||
{
|
|
||||||
ClientAction Action { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|
||||||
{
|
|
||||||
public interface ISocketResponse
|
|
||||||
{
|
|
||||||
string Action { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|
||||||
{
|
|
||||||
public class JoinByCodeRequest : IRequest
|
|
||||||
{
|
|
||||||
public ClientAction Action { get; set; }
|
|
||||||
public string JoinCode { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public class JoinGameRequest : IRequest
|
|
||||||
{
|
|
||||||
public ClientAction Action { get; set; }
|
|
||||||
public string GameName { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public class JoinGameResponse : ISocketResponse
|
|
||||||
{
|
|
||||||
public string Action { get; protected set; }
|
|
||||||
public string GameName { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The player who joined the game.
|
|
||||||
/// </summary>
|
|
||||||
public string PlayerName { get; set; }
|
|
||||||
|
|
||||||
public JoinGameResponse()
|
|
||||||
{
|
|
||||||
Action = ClientAction.JoinGame.ToString();
|
|
||||||
GameName = "";
|
|
||||||
PlayerName = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class JoinByCodeResponse : JoinGameResponse, ISocketResponse
|
|
||||||
{
|
|
||||||
public JoinByCodeResponse()
|
|
||||||
{
|
|
||||||
Action = ClientAction.JoinByCode.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|
||||||
{
|
|
||||||
public class MoveResponse : ISocketResponse
|
|
||||||
{
|
|
||||||
public string Action { get; }
|
|
||||||
public string GameName { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The player that made the move.
|
|
||||||
/// </summary>
|
|
||||||
public string PlayerName { get; set; }
|
|
||||||
|
|
||||||
public MoveResponse()
|
|
||||||
{
|
|
||||||
Action = ClientAction.Move.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
|
||||||
{
|
|
||||||
public enum ClientAction
|
|
||||||
{
|
|
||||||
CreateGame,
|
|
||||||
JoinGame,
|
|
||||||
JoinByCode,
|
|
||||||
Move
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
|
||||||
{
|
|
||||||
public enum WhichPlayer
|
|
||||||
{
|
|
||||||
Player1,
|
|
||||||
Player2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.0.31903.59
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets", "Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj", "{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.ServiceModels", "Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj", "{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.AcceptanceTests", "Shogi.AcceptanceTests\Shogi.AcceptanceTests.csproj", "{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.ActiveCfg = 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.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.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.Build.0 = Release|Any CPU
|
|
||||||
{F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.ActiveCfg = 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.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
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(NestedProjects) = preSolution
|
|
||||||
{F256989E-B6AF-4731-9DB4-88991C40B2CE} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
|
|
||||||
{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Managers;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
|
||||||
{
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("[controller]")]
|
|
||||||
[Authorize]
|
|
||||||
public class GameController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IGameboardManager gameboardManager;
|
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
|
||||||
private readonly ISocketConnectionManager communicationManager;
|
|
||||||
private readonly IModelMapper mapper;
|
|
||||||
|
|
||||||
public GameController(
|
|
||||||
IGameboardRepository repository,
|
|
||||||
IGameboardManager manager,
|
|
||||||
ISocketConnectionManager communicationManager,
|
|
||||||
IModelMapper mapper)
|
|
||||||
{
|
|
||||||
gameboardManager = manager;
|
|
||||||
gameboardRepository = repository;
|
|
||||||
this.communicationManager = communicationManager;
|
|
||||||
this.mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("JoinCode")]
|
|
||||||
public async Task<IActionResult> PostGameInvitation([FromBody] PostGameInvitation request)
|
|
||||||
{
|
|
||||||
|
|
||||||
//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]
|
|
||||||
[HttpPost("GuestJoinCode")]
|
|
||||||
public async Task<IActionResult> PostGuestGameInvitation([FromBody] PostGuestGameInvitation request)
|
|
||||||
{
|
|
||||||
|
|
||||||
//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)
|
|
||||||
{
|
|
||||||
var user = await gameboardManager.ReadUser(User);
|
|
||||||
var session = await gameboardRepository.ReadSession(gameName);
|
|
||||||
if (session == null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
if (user == null || (session.Player1Name != user.Id && session.Player2Name != user.Id))
|
|
||||||
{
|
|
||||||
return Forbid("User is not seated at this game.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var move = request.Move;
|
|
||||||
if (move.PieceFromCaptured.HasValue)
|
|
||||||
session.Move(mapper.Map(move.PieceFromCaptured.Value), move.To);
|
|
||||||
else if (!string.IsNullOrWhiteSpace(move.From))
|
|
||||||
session.Move(move.From, move.To, move.IsPromotion);
|
|
||||||
|
|
||||||
await gameboardRepository.CreateBoardState(session);
|
|
||||||
await communicationManager.BroadcastToPlayers(
|
|
||||||
new MoveResponse
|
|
||||||
{
|
|
||||||
GameName = session.Name,
|
|
||||||
PlayerName = user.Id
|
|
||||||
},
|
|
||||||
session.Player1Name,
|
|
||||||
session.Player2Name);
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
return Conflict(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use JWT tokens for guests so they can authenticate and use API routes, too.
|
|
||||||
//[Route("")]
|
|
||||||
//public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
|
||||||
//{
|
|
||||||
// var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2);
|
|
||||||
// var success = await repository.CreateSession(model);
|
|
||||||
// if (success)
|
|
||||||
// {
|
|
||||||
// var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.ClientAction.CreateGame)
|
|
||||||
// {
|
|
||||||
// Game = model.ToServiceModel(),
|
|
||||||
// PlayerName =
|
|
||||||
// }
|
|
||||||
// var task = request.IsPrivate
|
|
||||||
// ? communicationManager.BroadcastToPlayers(response, userName)
|
|
||||||
// : communicationManager.BroadcastToAll(response);
|
|
||||||
// return new CreatedResult("", null);
|
|
||||||
// }
|
|
||||||
// return new ConflictResult();
|
|
||||||
//}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
|
||||||
{
|
|
||||||
var user = await ReadUserOrThrow();
|
|
||||||
var session = new Shogi.Domain.SessionMetadata(request.Name, request.IsPrivate, user.Id);
|
|
||||||
await gameboardRepository.CreateSession(session);
|
|
||||||
await communicationManager.BroadcastToAll(new CreateGameResponse
|
|
||||||
{
|
|
||||||
Game = mapper.Map(session),
|
|
||||||
PlayerName = user.Id
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads the board session and subscribes the caller to socket events for that session.
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("{gameName}")]
|
|
||||||
public async Task<IActionResult> GetSession([FromRoute] string gameName)
|
|
||||||
{
|
|
||||||
var user = await ReadUserOrThrow();
|
|
||||||
var session = await gameboardRepository.ReadSession(gameName);
|
|
||||||
if (session == null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerPerspective = session.Player2Name == user.Id
|
|
||||||
? WhichPlayer.Player2
|
|
||||||
: WhichPlayer.Player1;
|
|
||||||
|
|
||||||
var response = new Session
|
|
||||||
{
|
|
||||||
BoardState = new BoardState
|
|
||||||
{
|
|
||||||
Board = mapper.Map(session.BoardState),
|
|
||||||
Player1Hand = session.Player1Hand.Select(mapper.Map).ToList(),
|
|
||||||
Player2Hand = session.Player2Hand.Select(mapper.Map).ToList(),
|
|
||||||
PlayerInCheck = mapper.Map(session.InCheck)
|
|
||||||
},
|
|
||||||
GameName = session.Name,
|
|
||||||
Player1 = session.Player1Name,
|
|
||||||
Player2 = session.Player2Name
|
|
||||||
};
|
|
||||||
return Ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<ActionResult<GetSessionsResponse>> GetSessions()
|
|
||||||
{
|
|
||||||
var user = await ReadUserOrThrow();
|
|
||||||
var sessions = await gameboardRepository.ReadSessionMetadatas();
|
|
||||||
|
|
||||||
var sessionsJoinedByUser = sessions
|
|
||||||
.Where(s => s.IsSeated(user.Id))
|
|
||||||
.Select(s => mapper.Map(s))
|
|
||||||
.ToList();
|
|
||||||
var sessionsNotJoinedByUser = sessions
|
|
||||||
.Where(s => !s.IsSeated(user.Id))
|
|
||||||
.Select(s => mapper.Map(s))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Ok(new GetSessionsResponse
|
|
||||||
{
|
|
||||||
PlayerHasJoinedSessions = sessionsJoinedByUser,
|
|
||||||
AllOtherSessions = sessionsNotJoinedByUser
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{gameName}")]
|
|
||||||
public async Task<IActionResult> PutJoinSession([FromRoute] string gameName)
|
|
||||||
{
|
|
||||||
var user = await ReadUserOrThrow();
|
|
||||||
var session = await gameboardRepository.ReadSessionMetaData(gameName);
|
|
||||||
if (session == null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
if (session.Player2 != null)
|
|
||||||
{
|
|
||||||
return this.Conflict("This session already has two seated players and is full.");
|
|
||||||
}
|
|
||||||
|
|
||||||
session.SetPlayer2(user.Id);
|
|
||||||
await gameboardRepository.UpdateSession(session);
|
|
||||||
|
|
||||||
var opponentName = user.Id == session.Player1
|
|
||||||
? session.Player2!
|
|
||||||
: session.Player1;
|
|
||||||
await communicationManager.BroadcastToPlayers(new JoinGameResponse
|
|
||||||
{
|
|
||||||
GameName = session.Name,
|
|
||||||
PlayerName = user.Id
|
|
||||||
}, opponentName);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize(Roles = "Admin, Shogi")]
|
|
||||||
[HttpDelete("{gameName}")]
|
|
||||||
public async Task<IActionResult> DeleteSession([FromRoute] string gameName)
|
|
||||||
{
|
|
||||||
var user = await ReadUserOrThrow();
|
|
||||||
if (user.IsAdmin)
|
|
||||||
{
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Unauthorized();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Models.User> ReadUserOrThrow()
|
|
||||||
{
|
|
||||||
var user = await gameboardManager.ReadUser(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException("Unknown user claims.");
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Managers;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("[controller]")]
|
|
||||||
[Authorize]
|
|
||||||
public class SocketController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly ISocketTokenCache tokenCache;
|
|
||||||
private readonly IGameboardManager gameboardManager;
|
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
|
||||||
private readonly ISocketConnectionManager connectionManager;
|
|
||||||
private readonly AuthenticationProperties authenticationProps;
|
|
||||||
|
|
||||||
public SocketController(
|
|
||||||
ILogger<SocketController> logger,
|
|
||||||
ISocketTokenCache tokenCache,
|
|
||||||
IGameboardManager gameboardManager,
|
|
||||||
IGameboardRepository gameboardRepository,
|
|
||||||
ISocketConnectionManager connectionManager)
|
|
||||||
{
|
|
||||||
this.tokenCache = tokenCache;
|
|
||||||
this.gameboardManager = gameboardManager;
|
|
||||||
this.gameboardRepository = gameboardRepository;
|
|
||||||
this.connectionManager = connectionManager;
|
|
||||||
|
|
||||||
authenticationProps = new AuthenticationProperties
|
|
||||||
{
|
|
||||||
AllowRefresh = true,
|
|
||||||
IsPersistent = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("GuestLogout")]
|
|
||||||
[AllowAnonymous]
|
|
||||||
public async Task<IActionResult> GuestLogout()
|
|
||||||
{
|
|
||||||
var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
|
|
||||||
var userId = User?.UserId();
|
|
||||||
if (!string.IsNullOrEmpty(userId))
|
|
||||||
{
|
|
||||||
connectionManager.Unsubscribe(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await signoutTask;
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("Token")]
|
|
||||||
public async Task<IActionResult> GetToken()
|
|
||||||
{
|
|
||||||
var user = await gameboardManager.ReadUser(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
await gameboardManager.CreateUser(User);
|
|
||||||
user = await gameboardManager.ReadUser(User);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = tokenCache.GenerateToken(user.Id);
|
|
||||||
return new JsonResult(new GetTokenResponse(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("GuestToken")]
|
|
||||||
[AllowAnonymous]
|
|
||||||
public async Task<IActionResult> GetGuestToken()
|
|
||||||
{
|
|
||||||
var user = await gameboardManager.ReadUser(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
// Create a guest user.
|
|
||||||
var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString());
|
|
||||||
await gameboardRepository.CreateUser(newUser);
|
|
||||||
|
|
||||||
var identity = newUser.CreateClaimsIdentity();
|
|
||||||
await HttpContext.SignInAsync(
|
|
||||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
|
||||||
new ClaimsPrincipal(identity),
|
|
||||||
authenticationProps
|
|
||||||
);
|
|
||||||
user = newUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = tokenCache.GenerateToken(user.Id.ToString());
|
|
||||||
return this.Ok(new GetGuestTokenResponse(user.Id, user.DisplayName, token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Extensions
|
|
||||||
{
|
|
||||||
public static class Extensions
|
|
||||||
{
|
|
||||||
private static readonly string MsalUsernameClaim = "preferred_username";
|
|
||||||
|
|
||||||
public static string? UserId(this ClaimsPrincipal self)
|
|
||||||
{
|
|
||||||
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string? DisplayName(this ClaimsPrincipal self)
|
|
||||||
{
|
|
||||||
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsMicrosoft(this ClaimsPrincipal self)
|
|
||||||
{
|
|
||||||
return self.HasClaim(c => c.Type == MsalUsernameClaim);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ChangeFirstCharacterToLowerCase(this string self)
|
|
||||||
{
|
|
||||||
return char.ToLowerInvariant(self[0]) + self[1..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
|
||||||
{
|
|
||||||
public interface IJoinByCodeHandler
|
|
||||||
{
|
|
||||||
Task Handle(JoinByCodeRequest request, string userName);
|
|
||||||
}
|
|
||||||
public class JoinByCodeHandler : IJoinByCodeHandler
|
|
||||||
{
|
|
||||||
private readonly IGameboardRepository repository;
|
|
||||||
private readonly ISocketConnectionManager communicationManager;
|
|
||||||
|
|
||||||
public JoinByCodeHandler(
|
|
||||||
ISocketConnectionManager communicationManager,
|
|
||||||
IGameboardRepository repository)
|
|
||||||
{
|
|
||||||
this.repository = repository;
|
|
||||||
this.communicationManager = communicationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(JoinByCodeRequest request, string userName)
|
|
||||||
{
|
|
||||||
//var request = JsonConvert.DeserializeObject<JoinByCode>(json);
|
|
||||||
//var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession
|
|
||||||
//{
|
|
||||||
// PlayerName = userName,
|
|
||||||
// JoinCode = request.JoinCode
|
|
||||||
//});
|
|
||||||
|
|
||||||
//if (sessionName == null)
|
|
||||||
//{
|
|
||||||
// var response = new JoinGameResponse(ClientAction.JoinByCode)
|
|
||||||
// {
|
|
||||||
// PlayerName = userName,
|
|
||||||
// GameName = sessionName,
|
|
||||||
// Error = "Error joining game."
|
|
||||||
// };
|
|
||||||
// await communicationManager.BroadcastToPlayers(response, userName);
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
//{
|
|
||||||
// // Other members of the game see a regular JoinGame occur.
|
|
||||||
// var response = new JoinGameResponse(ClientAction.JoinGame)
|
|
||||||
// {
|
|
||||||
// PlayerName = userName,
|
|
||||||
// GameName = sessionName
|
|
||||||
// };
|
|
||||||
// // At this time, userName hasn't subscribed and won't receive this message.
|
|
||||||
// await communicationManager.BroadcastToGame(sessionName, response);
|
|
||||||
|
|
||||||
// // The player joining sees the JoinByCode occur.
|
|
||||||
// response = new JoinGameResponse(ClientAction.JoinByCode)
|
|
||||||
// {
|
|
||||||
// PlayerName = userName,
|
|
||||||
// GameName = sessionName
|
|
||||||
// };
|
|
||||||
// await communicationManager.BroadcastToPlayers(response, userName);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Models;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
|
||||||
using System;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers
|
|
||||||
{
|
|
||||||
public interface IGameboardManager
|
|
||||||
{
|
|
||||||
Task AssignPlayer2ToSession(string sessionName, User user);
|
|
||||||
Task<User?> ReadUser(ClaimsPrincipal user);
|
|
||||||
Task<User?> CreateUser(ClaimsPrincipal user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GameboardManager : IGameboardManager
|
|
||||||
{
|
|
||||||
private readonly IGameboardRepository repository;
|
|
||||||
|
|
||||||
public GameboardManager(IGameboardRepository repository)
|
|
||||||
{
|
|
||||||
this.repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User> CreateUser(ClaimsPrincipal principal)
|
|
||||||
{
|
|
||||||
var id = principal.UserId();
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Cannot create user from given claims.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = principal.IsMicrosoft()
|
|
||||||
? User.CreateMsalUser(id)
|
|
||||||
: User.CreateGuestUser(id);
|
|
||||||
|
|
||||||
await repository.CreateUser(user);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<User?> ReadUser(ClaimsPrincipal principal)
|
|
||||||
{
|
|
||||||
var userId = principal.UserId();
|
|
||||||
if (!string.IsNullOrEmpty(userId))
|
|
||||||
{
|
|
||||||
return repository.ReadUser(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult<User?>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<string> CreateJoinCode(string sessionName, string playerName)
|
|
||||||
{
|
|
||||||
//var session = await repository.GetGame(sessionName);
|
|
||||||
//if (playerName == session?.Player1)
|
|
||||||
//{
|
|
||||||
// return await repository.PostJoinCode(sessionName, playerName);
|
|
||||||
//}
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AssignPlayer2ToSession(string sessionName, User user)
|
|
||||||
{
|
|
||||||
var session = await repository.ReadSessionMetaData(sessionName);
|
|
||||||
if (session != null && !session.IsPrivate && session.Player2 == null)
|
|
||||||
{
|
|
||||||
session.SetPlayer2(user.Id);
|
|
||||||
await repository.UpdateSession(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers
|
|
||||||
{
|
|
||||||
public interface ISocketConnectionManager
|
|
||||||
{
|
|
||||||
Task BroadcastToAll(ISocketResponse response);
|
|
||||||
void Subscribe(WebSocket socket, string playerName);
|
|
||||||
void Unsubscribe(string playerName);
|
|
||||||
Task BroadcastToPlayers(ISocketResponse response, params string?[] playerNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retains all active socket connections and provides convenient methods for sending messages to clients.
|
|
||||||
/// </summary>
|
|
||||||
public class SocketConnectionManager : ISocketConnectionManager
|
|
||||||
{
|
|
||||||
/// <summary>Dictionary key is player name.</summary>
|
|
||||||
private readonly ConcurrentDictionary<string, WebSocket> connections;
|
|
||||||
/// <summary>Dictionary key is game name.</summary>
|
|
||||||
private readonly ILogger<SocketConnectionManager> logger;
|
|
||||||
|
|
||||||
public SocketConnectionManager(ILogger<SocketConnectionManager> logger)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
connections = new ConcurrentDictionary<string, WebSocket>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Subscribe(WebSocket socket, string playerName)
|
|
||||||
{
|
|
||||||
connections.TryRemove(playerName, out var _);
|
|
||||||
connections.TryAdd(playerName, socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Unsubscribe(string playerName)
|
|
||||||
{
|
|
||||||
connections.TryRemove(playerName, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BroadcastToPlayers(ISocketResponse response, params string?[] playerNames)
|
|
||||||
{
|
|
||||||
var tasks = new List<Task>(playerNames.Length);
|
|
||||||
foreach (var name in playerNames)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
public Task BroadcastToAll(ISocketResponse response)
|
|
||||||
{
|
|
||||||
var message = JsonConvert.SerializeObject(response);
|
|
||||||
logger.LogInformation($"Broadcasting\n{0}", message);
|
|
||||||
var tasks = new List<Task>(connections.Count);
|
|
||||||
foreach (var kvp in connections)
|
|
||||||
{
|
|
||||||
var socket = kvp.Value;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
tasks.Add(socket.SendTextAsync(message));
|
|
||||||
}
|
|
||||||
catch (WebSocketException)
|
|
||||||
{
|
|
||||||
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
|
|
||||||
Unsubscribe(kvp.Key);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
|
|
||||||
Unsubscribe(kvp.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var task = Task.WhenAll(tasks);
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Console.WriteLine("Yo");
|
|
||||||
}
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Models
|
|
||||||
{
|
|
||||||
public class User
|
|
||||||
{
|
|
||||||
public static readonly ReadOnlyCollection<string> Adjectives = new(new[] {
|
|
||||||
"Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly"
|
|
||||||
});
|
|
||||||
public static readonly ReadOnlyCollection<string> Subjects = new(new[] {
|
|
||||||
"Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato", "Penguin"
|
|
||||||
});
|
|
||||||
public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft);
|
|
||||||
public static User CreateGuestUser(string id)
|
|
||||||
{
|
|
||||||
var random = new Random();
|
|
||||||
// Adjective
|
|
||||||
var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count);
|
|
||||||
var adj = Adjectives[index];
|
|
||||||
// Subject
|
|
||||||
index = (int)Math.Floor(random.NextDouble() * Subjects.Count);
|
|
||||||
var subj = Subjects[index];
|
|
||||||
|
|
||||||
return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; }
|
|
||||||
public string DisplayName { get; }
|
|
||||||
|
|
||||||
public WhichLoginPlatform LoginPlatform { get; }
|
|
||||||
|
|
||||||
public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest;
|
|
||||||
|
|
||||||
public bool IsAdmin => LoginPlatform == WhichLoginPlatform.Microsoft && Id == "Hauth@live.com";
|
|
||||||
|
|
||||||
public User(string id, string displayName, WhichLoginPlatform platform)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
DisplayName = displayName;
|
|
||||||
LoginPlatform = platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public User(UserDocument document)
|
|
||||||
{
|
|
||||||
Id = document.Id;
|
|
||||||
DisplayName = document.DisplayName;
|
|
||||||
LoginPlatform = document.Platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClaimsIdentity CreateClaimsIdentity()
|
|
||||||
{
|
|
||||||
if (LoginPlatform == WhichLoginPlatform.Guest)
|
|
||||||
{
|
|
||||||
var claims = new List<Claim>(4)
|
|
||||||
{
|
|
||||||
new Claim(ClaimTypes.NameIdentifier, Id),
|
|
||||||
new Claim(ClaimTypes.Name, DisplayName),
|
|
||||||
new Claim(ClaimTypes.Role, "Guest"),
|
|
||||||
};
|
|
||||||
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var claims = new List<Claim>(3)
|
|
||||||
{
|
|
||||||
new Claim(ClaimTypes.NameIdentifier, Id),
|
|
||||||
new Claim(ClaimTypes.Name, DisplayName),
|
|
||||||
};
|
|
||||||
return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServiceModels.Types.User ToServiceModel() => new()
|
|
||||||
{
|
|
||||||
Id = Id,
|
|
||||||
Name = DisplayName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
|
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Shogi.Domain;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories
|
|
||||||
{
|
|
||||||
public interface IGameboardRepository
|
|
||||||
{
|
|
||||||
Task CreateBoardState(Session session);
|
|
||||||
Task CreateSession(SessionMetadata session);
|
|
||||||
Task CreateUser(Models.User user);
|
|
||||||
Task<Collection<SessionMetadata>> ReadSessionMetadatas();
|
|
||||||
Task<Session?> ReadSession(string name);
|
|
||||||
Task UpdateSession(SessionMetadata session);
|
|
||||||
Task<SessionMetadata?> ReadSessionMetaData(string name);
|
|
||||||
Task<Models.User?> ReadUser(string userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GameboardRepository : IGameboardRepository
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns session, board state, and user documents, grouped by session.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate";
|
|
||||||
/// <summary>
|
|
||||||
/// Returns session and user documents, grouped by session.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata";
|
|
||||||
private static readonly string View_User = "_design/user/_view/user";
|
|
||||||
private const string ApplicationJson = "application/json";
|
|
||||||
private readonly HttpClient client;
|
|
||||||
private readonly ILogger<GameboardRepository> logger;
|
|
||||||
|
|
||||||
public GameboardRepository(IHttpClientFactory clientFactory, ILogger<GameboardRepository> logger)
|
|
||||||
{
|
|
||||||
client = clientFactory.CreateClient("couchdb");
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Collection<SessionMetadata>> ReadSessionMetadatas()
|
|
||||||
{
|
|
||||||
var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
|
|
||||||
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
|
||||||
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
var groupedBySession = result.rows.GroupBy(row => row.id);
|
|
||||||
var sessions = new List<SessionMetadata>(result.total_rows / 3);
|
|
||||||
foreach (var group in groupedBySession)
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A group contains 3 elements.
|
|
||||||
* 1) The session metadata.
|
|
||||||
* 2) User document of Player1.
|
|
||||||
* 3) User document of Player2.
|
|
||||||
*/
|
|
||||||
var session = group.FirstOrDefault()?.doc.ToObject<SessionDocument>();
|
|
||||||
var player1 = group.Skip(1).FirstOrDefault()?.doc.ToObject<UserDocument>();
|
|
||||||
var player2Doc = group.Skip(2).FirstOrDefault()?.doc;
|
|
||||||
if (session != null && player1 != null && player2Doc != null)
|
|
||||||
{
|
|
||||||
var player2 = IsUserDocument(player2Doc)
|
|
||||||
? new Models.User(player2Doc.ToObject<UserDocument>()!)
|
|
||||||
: null;
|
|
||||||
sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Collection<SessionMetadata>(sessions);
|
|
||||||
}
|
|
||||||
return new Collection<SessionMetadata>(Array.Empty<SessionMetadata>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsUserDocument(JObject player2Doc)
|
|
||||||
{
|
|
||||||
return player2Doc?.SelectToken(nameof(CouchDocument.DocumentType))?.Value<WhichDocumentType>() == WhichDocumentType.User;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Session?> ReadSession(string name)
|
|
||||||
{
|
|
||||||
static Shogi.Domain.Pieces.Piece? MapPiece(Piece? piece)
|
|
||||||
{
|
|
||||||
return piece == null
|
|
||||||
? null
|
|
||||||
: Shogi.Domain.Pieces.Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryParams = new QueryBuilder
|
|
||||||
{
|
|
||||||
{ "include_docs", "true" },
|
|
||||||
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
|
|
||||||
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
|
|
||||||
}.ToQueryString();
|
|
||||||
var query = $"{View_SessionWithBoardState}{queryParams}";
|
|
||||||
logger.LogInformation("ReadSession() query: {query}", query);
|
|
||||||
var response = await client.GetAsync(query);
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
|
||||||
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
|
||||||
if (result != null && result.rows.Length > 2)
|
|
||||||
{
|
|
||||||
var group = result.rows;
|
|
||||||
/**
|
|
||||||
* A group contains multiple elements.
|
|
||||||
* 0) The session metadata.
|
|
||||||
* 1) User documents of Player1.
|
|
||||||
* 2) User documents of Player1.
|
|
||||||
* 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :(
|
|
||||||
* Everything Else) Snapshots of the boardstate after every player move.
|
|
||||||
*/
|
|
||||||
var session = group[0].doc.ToObject<SessionDocument>();
|
|
||||||
var player1 = group[1].doc.ToObject<UserDocument>();
|
|
||||||
var player2Doc = group[2].doc;
|
|
||||||
var boardState = group.Last().doc.ToObject<BoardStateDocument>();
|
|
||||||
|
|
||||||
if (session != null && player1 != null && boardState != null)
|
|
||||||
{
|
|
||||||
var player2 = IsUserDocument(player2Doc)
|
|
||||||
? new Models.User(player2Doc.ToObject<UserDocument>()!)
|
|
||||||
: null;
|
|
||||||
var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id);
|
|
||||||
var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value)));
|
|
||||||
return new Session(shogiBoardState, metaData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SessionMetadata?> ReadSessionMetaData(string name)
|
|
||||||
{
|
|
||||||
var queryParams = new QueryBuilder
|
|
||||||
{
|
|
||||||
{ "include_docs", "true" },
|
|
||||||
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
|
|
||||||
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
|
|
||||||
}.ToQueryString();
|
|
||||||
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
|
||||||
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
|
||||||
if (result != null && result.rows.Length > 2)
|
|
||||||
{
|
|
||||||
var group = result.rows;
|
|
||||||
/**
|
|
||||||
* A group contains 3 elements.
|
|
||||||
* 1) The session metadata.
|
|
||||||
* 2) User document of Player1.
|
|
||||||
* 3) User document of Player2.
|
|
||||||
*/
|
|
||||||
var session = group[0].doc.ToObject<SessionDocument>();
|
|
||||||
var player1 = group[1].doc.ToObject<UserDocument>();
|
|
||||||
var player2Doc = group[2].doc;
|
|
||||||
if (session != null && player1 != null)
|
|
||||||
{
|
|
||||||
var player2 = IsUserDocument(player2Doc)
|
|
||||||
? new Models.User(player2Doc.ToObject<UserDocument>()!)
|
|
||||||
: null;
|
|
||||||
return new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves a snapshot of board state and the most recent move.
|
|
||||||
/// </summary>
|
|
||||||
public async Task CreateBoardState(Session session)
|
|
||||||
{
|
|
||||||
var boardStateDocument = new BoardStateDocument(session.Name, session);
|
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
|
||||||
var response = await client.PostAsync(string.Empty, content);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateSession(SessionMetadata session)
|
|
||||||
{
|
|
||||||
var sessionDocument = new SessionDocument(session);
|
|
||||||
var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
|
|
||||||
var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent);
|
|
||||||
|
|
||||||
var boardStateDocument = new BoardStateDocument(session.Name, new Session());
|
|
||||||
var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
|
||||||
|
|
||||||
if ((await postSessionDocumentTask).IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var response = await client.PostAsync(string.Empty, boardStateContent);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateSession(SessionMetadata session)
|
|
||||||
{
|
|
||||||
// GET existing session to get revisionId.
|
|
||||||
var readResponse = await client.GetAsync(session.Name);
|
|
||||||
readResponse.EnsureSuccessStatusCode();
|
|
||||||
var sessionDocument = JsonConvert.DeserializeObject<SessionDocument>(await readResponse.Content.ReadAsStringAsync());
|
|
||||||
|
|
||||||
// PUT the document with the revisionId.
|
|
||||||
var couchModel = new SessionDocument(session)
|
|
||||||
{
|
|
||||||
RevisionId = sessionDocument?.RevisionId
|
|
||||||
};
|
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
|
|
||||||
var response = await client.PutAsync(couchModel.Id, content);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
//public async Task<bool> PutJoinPublicSession(PutJoinPublicSession request)
|
|
||||||
//{
|
|
||||||
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
|
|
||||||
// var response = await client.PutAsync(JoinSessionRoute, content);
|
|
||||||
// var json = await response.Content.ReadAsStringAsync();
|
|
||||||
// return JsonConvert.DeserializeObject<PutJoinPublicSessionResponse>(json).JoinSucceeded;
|
|
||||||
//}
|
|
||||||
|
|
||||||
//public async Task<string> PostJoinPrivateSession(PostJoinPrivateSession request)
|
|
||||||
//{
|
|
||||||
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
|
|
||||||
// var response = await client.PostAsync(JoinSessionRoute, content);
|
|
||||||
// var json = await response.Content.ReadAsStringAsync();
|
|
||||||
// var deserialized = JsonConvert.DeserializeObject<PostJoinPrivateSessionResponse>(json);
|
|
||||||
// if (deserialized.JoinSucceeded)
|
|
||||||
// {
|
|
||||||
// return deserialized.SessionName;
|
|
||||||
// }
|
|
||||||
// return null;
|
|
||||||
//}
|
|
||||||
|
|
||||||
//public async Task<List<Move>> GetMoves(string gameName)
|
|
||||||
//{
|
|
||||||
// var uri = $"Session/{gameName}/Moves";
|
|
||||||
// var get = await client.GetAsync(Uri.EscapeUriString(uri));
|
|
||||||
// var json = await get.Content.ReadAsStringAsync();
|
|
||||||
// if (string.IsNullOrWhiteSpace(json))
|
|
||||||
// {
|
|
||||||
// return new List<Move>();
|
|
||||||
// }
|
|
||||||
// var response = JsonConvert.DeserializeObject<GetMovesResponse>(json);
|
|
||||||
// return response.Moves.Select(m => new Move(m)).ToList();
|
|
||||||
//}
|
|
||||||
|
|
||||||
//public async Task PostMove(string gameName, PostMove request)
|
|
||||||
//{
|
|
||||||
// var uri = $"Session/{gameName}/Move";
|
|
||||||
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
|
|
||||||
// await client.PostAsync(Uri.EscapeUriString(uri), content);
|
|
||||||
//}
|
|
||||||
|
|
||||||
public async Task<string> PostJoinCode(string gameName, string userName)
|
|
||||||
{
|
|
||||||
// var uri = $"JoinCode/{gameName}";
|
|
||||||
// var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName });
|
|
||||||
// var content = new StringContent(serialized, Encoding.UTF8, MediaType);
|
|
||||||
// var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync();
|
|
||||||
// return JsonConvert.DeserializeObject<PostJoinCodeResponse>(json).JoinCode;
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Models.User?> ReadUser(string id)
|
|
||||||
{
|
|
||||||
var queryParams = new QueryBuilder
|
|
||||||
{
|
|
||||||
{ "include_docs", "true" },
|
|
||||||
{ "key", JsonConvert.SerializeObject(id) },
|
|
||||||
}.ToQueryString();
|
|
||||||
var response = await client.GetAsync($"{View_User}{queryParams}");
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
|
||||||
var result = JsonConvert.DeserializeObject<CouchViewResult<UserDocument>>(responseContent);
|
|
||||||
if (result != null && result.rows.Length > 0)
|
|
||||||
{
|
|
||||||
return new Models.User(result.rows[0].doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateUser(Models.User user)
|
|
||||||
{
|
|
||||||
var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform);
|
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
|
|
||||||
var response = await client.PostAsync(string.Empty, content);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReadMoveHistory()
|
|
||||||
{
|
|
||||||
//TODO: Separate move history into a separate request.
|
|
||||||
//var moves = group
|
|
||||||
// .Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session.
|
|
||||||
// // TODO: Deserialize just the Move property.
|
|
||||||
// .Select(row => row.doc.ToObject<BoardStateDocument>())
|
|
||||||
// .Select(boardState =>
|
|
||||||
// {
|
|
||||||
// var move = boardState!.Move!;
|
|
||||||
// return move.PieceFromHand.HasValue
|
|
||||||
// ? new Models.Move(move.PieceFromHand.Value, move.To)
|
|
||||||
// : new Models.Move(move.From!, move.To, move.IsPromotion);
|
|
||||||
// })
|
|
||||||
// .ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
|
||||||
{
|
|
||||||
public class JoinByCodeRequestValidator : AbstractValidator<JoinByCodeRequest>
|
|
||||||
{
|
|
||||||
public JoinByCodeRequestValidator()
|
|
||||||
{
|
|
||||||
RuleFor(_ => _.Action).Equal(ClientAction.JoinByCode);
|
|
||||||
RuleFor(_ => _.JoinCode).NotEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
|
|
||||||
{
|
|
||||||
public class JoinGameRequestValidator : AbstractValidator<JoinGameRequest>
|
|
||||||
{
|
|
||||||
public JoinGameRequestValidator()
|
|
||||||
{
|
|
||||||
RuleFor(_ => _.Action).Equal(ClientAction.JoinGame);
|
|
||||||
RuleFor(_ => _.GameName).NotEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Standardizes the claims from third party issuers. Also registers new msal users in the database.
|
|
||||||
/// </summary>
|
|
||||||
public class ShogiUserClaimsTransformer : IClaimsTransformation
|
|
||||||
{
|
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
|
||||||
|
|
||||||
public ShogiUserClaimsTransformer(IGameboardRepository gameboardRepository)
|
|
||||||
{
|
|
||||||
this.gameboardRepository = gameboardRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
|
||||||
{
|
|
||||||
var id = principal.UserId();
|
|
||||||
if (!string.IsNullOrWhiteSpace(id))
|
|
||||||
{
|
|
||||||
var user = await gameboardRepository.ReadUser(id);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
var newUser = principal.IsMicrosoft()
|
|
||||||
? Models.User.CreateMsalUser(id)
|
|
||||||
: Models.User.CreateGuestUser(id);
|
|
||||||
|
|
||||||
await gameboardRepository.CreateUser(newUser);
|
|
||||||
user = newUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
return new ClaimsPrincipal(user.CreateClaimsIdentity());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return principal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using Shogi.AcceptanceTests.TestSetup;
|
|
||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace Shogi.AcceptanceTests
|
|
||||||
{
|
|
||||||
public class AcceptanceTests : IClassFixture<AATFixture>
|
|
||||||
{
|
|
||||||
private readonly AATFixture fixture;
|
|
||||||
private readonly ITestOutputHelper console;
|
|
||||||
|
|
||||||
public AcceptanceTests(AATFixture fixture, ITestOutputHelper console)
|
|
||||||
{
|
|
||||||
this.fixture = fixture;
|
|
||||||
this.console = console;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateAndReadSession()
|
|
||||||
{
|
|
||||||
var response = await fixture.Service.GetAsync(new Uri("Game", UriKind.Relative));
|
|
||||||
console.WriteLine(await response.Content.ReadAsStringAsync());
|
|
||||||
console.WriteLine(response.Headers.WwwAuthenticate.ToString());
|
|
||||||
response.IsSuccessStatusCode.Should().BeTrue(because: "AAT Client should be authorized.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
Shogi.Contracts/Api/Commands/CreateGuestTokenResponse.cs
Normal file
17
Shogi.Contracts/Api/Commands/CreateGuestTokenResponse.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Api;
|
||||||
|
|
||||||
|
public class CreateGuestTokenResponse
|
||||||
|
{
|
||||||
|
public string UserId { get; }
|
||||||
|
public string DisplayName { get; }
|
||||||
|
public Guid OneTimeToken { get; }
|
||||||
|
|
||||||
|
public CreateGuestTokenResponse(string userId, string displayName, Guid oneTimeToken)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
DisplayName = displayName;
|
||||||
|
OneTimeToken = oneTimeToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Shogi.Contracts/Api/Commands/CreateSessionCommand.cs
Normal file
10
Shogi.Contracts/Api/Commands/CreateSessionCommand.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Api;
|
||||||
|
|
||||||
|
public class CreateSessionCommand
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public bool IsPrivate { get; set; }
|
||||||
|
}
|
||||||
13
Shogi.Contracts/Api/Commands/CreateTokenResponse.cs
Normal file
13
Shogi.Contracts/Api/Commands/CreateTokenResponse.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Api;
|
||||||
|
|
||||||
|
public class CreateTokenResponse
|
||||||
|
{
|
||||||
|
public Guid OneTimeToken { get; }
|
||||||
|
|
||||||
|
public CreateTokenResponse(Guid token)
|
||||||
|
{
|
||||||
|
OneTimeToken = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Shogi.Contracts/Api/Commands/MovePieceCommand.cs
Normal file
10
Shogi.Contracts/Api/Commands/MovePieceCommand.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Shogi.Contracts.Types;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Api;
|
||||||
|
|
||||||
|
public class MovePieceCommand
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public Move Move { get; set; }
|
||||||
|
}
|
||||||
10
Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs
Normal file
10
Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Shogi.Contracts.Types;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Api;
|
||||||
|
|
||||||
|
public class ReadAllSessionsResponse
|
||||||
|
{
|
||||||
|
public IList<SessionMetadata> PlayerHasJoinedSessions { get; set; }
|
||||||
|
public IList<SessionMetadata> AllOtherSessions { get; set; }
|
||||||
|
}
|
||||||
8
Shogi.Contracts/Api/Queries/ReadSessionResponse.cs
Normal file
8
Shogi.Contracts/Api/Queries/ReadSessionResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Shogi.Contracts.Types;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Api;
|
||||||
|
|
||||||
|
public class ReadSessionResponse
|
||||||
|
{
|
||||||
|
public Session Session { get; set; }
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||||
<AnalysisLevel>5</AnalysisLevel>
|
<AnalysisLevel>5</AnalysisLevel>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||||
<Title>Shogi Service Models</Title>
|
<Title>Shogi Service Models</Title>
|
||||||
<Description>Contains DTOs use for http requests to Shogi backend services.</Description>
|
<Description>Contains DTOs use for http requests to Shogi backend services.</Description>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
13
Shogi.Contracts/Socket/CreateGame.cs
Normal file
13
Shogi.Contracts/Socket/CreateGame.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Shogi.Contracts.Types;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Socket;
|
||||||
|
|
||||||
|
public class SessionCreatedSocketMessage : ISocketResponse
|
||||||
|
{
|
||||||
|
public SocketAction Action { get; }
|
||||||
|
|
||||||
|
public SessionCreatedSocketMessage()
|
||||||
|
{
|
||||||
|
Action = SocketAction.SessionCreated;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Shogi.Contracts/Socket/ISocketRequest.cs
Normal file
9
Shogi.Contracts/Socket/ISocketRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Shogi.Contracts.Types;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Socket
|
||||||
|
{
|
||||||
|
public interface ISocketRequest
|
||||||
|
{
|
||||||
|
SocketAction Action { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Shogi.Contracts/Socket/ISocketResponse.cs
Normal file
13
Shogi.Contracts/Socket/ISocketResponse.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Shogi.Contracts.Types;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Socket;
|
||||||
|
|
||||||
|
public interface ISocketResponse
|
||||||
|
{
|
||||||
|
SocketAction Action { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SocketResponse : ISocketResponse
|
||||||
|
{
|
||||||
|
public SocketAction Action { get; set; }
|
||||||
|
}
|
||||||
18
Shogi.Contracts/Socket/Move.cs
Normal file
18
Shogi.Contracts/Socket/Move.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Shogi.Contracts.Types;
|
||||||
|
|
||||||
|
namespace Shogi.Contracts.Socket;
|
||||||
|
|
||||||
|
public class MoveResponse : ISocketResponse
|
||||||
|
{
|
||||||
|
public SocketAction Action { get; }
|
||||||
|
public string SessionName { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// The player that made the move.
|
||||||
|
/// </summary>
|
||||||
|
public string PlayerName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public MoveResponse()
|
||||||
|
{
|
||||||
|
Action = SocketAction.PieceMoved;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
namespace Shogi.Contracts.Types
|
||||||
{
|
{
|
||||||
public class BoardState
|
public class BoardState
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
namespace Shogi.Contracts.Types
|
||||||
{
|
{
|
||||||
public class Move
|
public class Move
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
namespace Shogi.Contracts.Types
|
||||||
{
|
{
|
||||||
public class Piece
|
public class Piece
|
||||||
{
|
{
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
namespace Shogi.Contracts.Types
|
||||||
{
|
{
|
||||||
public class Session
|
public class Session
|
||||||
{
|
{
|
||||||
public string Player1 { get; set; }
|
public string Player1 { get; set; }
|
||||||
public string? Player2 { get; set; }
|
public string? Player2 { get; set; }
|
||||||
public string GameName { get; set; }
|
public string SessionName { get; set; }
|
||||||
public BoardState BoardState { get; set; }
|
public BoardState BoardState { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
Shogi.Contracts/Types/SessionMetadata.cs
Normal file
8
Shogi.Contracts/Types/SessionMetadata.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Shogi.Contracts.Types
|
||||||
|
{
|
||||||
|
public class SessionMetadata
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int PlayerCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Shogi.Contracts/Types/SocketAction.cs
Normal file
9
Shogi.Contracts/Types/SocketAction.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Shogi.Contracts.Types
|
||||||
|
{
|
||||||
|
public enum SocketAction
|
||||||
|
{
|
||||||
|
SessionCreated,
|
||||||
|
SessionJoined,
|
||||||
|
PieceMoved
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
namespace Shogi.Contracts.Types
|
||||||
{
|
{
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
8
Shogi.Contracts/Types/WhichPerspective.cs
Normal file
8
Shogi.Contracts/Types/WhichPerspective.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Shogi.Contracts.Types
|
||||||
|
{
|
||||||
|
public enum WhichPlayer
|
||||||
|
{
|
||||||
|
Player1,
|
||||||
|
Player2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
namespace Shogi.Contracts.Types
|
||||||
{
|
{
|
||||||
public enum WhichPiece
|
public enum WhichPiece
|
||||||
{
|
{
|
||||||
13
Shogi.Database/Post Deployment/Script.PostDeployment.sql
Normal file
13
Shogi.Database/Post Deployment/Script.PostDeployment.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
Post-Deployment Script Template
|
||||||
|
--------------------------------------------------------------------------------------
|
||||||
|
This file contains SQL statements that will be appended to the build script.
|
||||||
|
Use SQLCMD syntax to include a file in the post-deployment script.
|
||||||
|
Example: :r .\myfile.sql
|
||||||
|
Use SQLCMD syntax to reference a variable in the post-deployment script.
|
||||||
|
Example: :setvar TableName MyTable
|
||||||
|
SELECT * FROM [$(TableName)]
|
||||||
|
--------------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
:r .\Scripts\PopulateLoginPlatforms.sql
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
DECLARE @LoginPlatforms TABLE (
|
||||||
|
[Platform] NVARCHAR(20)
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO @LoginPlatforms ([Platform])
|
||||||
|
VALUES
|
||||||
|
('Guest'),
|
||||||
|
('Microsoft');
|
||||||
|
|
||||||
|
MERGE [user].[LoginPlatform] as t
|
||||||
|
USING @LoginPlatforms as s
|
||||||
|
ON t.[Platform] = s.[Platform]
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT ([Platform])
|
||||||
|
VALUES (s.[Platform]);
|
||||||
1
Shogi.Database/Session/Session.sql
Normal file
1
Shogi.Database/Session/Session.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE SCHEMA [session]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[CreateBoardState]
|
||||||
|
@boardStateDocument NVARCHAR(max),
|
||||||
|
@sessionName NVARCHAR(100)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [session].[BoardState] (Document, SessionId)
|
||||||
|
SELECT
|
||||||
|
@boardStateDocument,
|
||||||
|
Id
|
||||||
|
FROM [session].[Session]
|
||||||
|
WHERE [Name] = @sessionName;
|
||||||
|
|
||||||
|
END
|
||||||
24
Shogi.Database/Session/Stored Procedures/CreateSession.sql
Normal file
24
Shogi.Database/Session/Stored Procedures/CreateSession.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE PROCEDURE [session].[CreateSession]
|
||||||
|
@SessionName [session].[SessionName],
|
||||||
|
@Player1Name [user].[UserName],
|
||||||
|
@InitialBoardStateDocument [session].[JsonDocument]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
SET NOCOUNT ON
|
||||||
|
SET XACT_ABORT ON
|
||||||
|
|
||||||
|
BEGIN TRANSACTION
|
||||||
|
INSERT INTO [session].[Session] ([Name], Player1Id)
|
||||||
|
SELECT
|
||||||
|
@SessionName,
|
||||||
|
Id
|
||||||
|
FROM [user].[User]
|
||||||
|
WHERE [Name] = @Player1Name;
|
||||||
|
|
||||||
|
INSERT INTO [session].[BoardState] (Document, SessionId)
|
||||||
|
VALUES
|
||||||
|
(@InitialBoardStateDocument, SCOPE_IDENTITY());
|
||||||
|
COMMIT
|
||||||
|
|
||||||
|
END
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE PROCEDURE [session].[ReadAllSessionsMetadata]
|
||||||
|
AS
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
[Name],
|
||||||
|
CASE Player2Id
|
||||||
|
WHEN NULL THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END AS PlayerCount
|
||||||
|
FROM [session].[Session];
|
||||||
14
Shogi.Database/Session/Stored Procedures/UpdateSession.sql
Normal file
14
Shogi.Database/Session/Stored Procedures/UpdateSession.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[UpdateSession]
|
||||||
|
@SessionName [session].[SessionName],
|
||||||
|
@BoardStateJson [session].[JsonDocument]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
UPDATE bs
|
||||||
|
SET bs.Document = @BoardStateJson
|
||||||
|
FROM [session].[BoardState] bs
|
||||||
|
INNER JOIN [session].[Session] s on s.Id = bs.SessionId
|
||||||
|
WHERE s.Name = @SessionName;
|
||||||
|
|
||||||
|
END
|
||||||
10
Shogi.Database/Session/Tables/BoardState.sql
Normal file
10
Shogi.Database/Session/Tables/BoardState.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE [session].[BoardState]
|
||||||
|
(
|
||||||
|
[Id] BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||||
|
[Document] NVARCHAR(max) NOT NULL,
|
||||||
|
[SessionId] BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT [Document must be json] CHECK (isjson(Document)=1),
|
||||||
|
CONSTRAINT FK_BoardState_Session FOREIGN KEY (SessionId)
|
||||||
|
REFERENCES [session].[Session] (Id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
)
|
||||||
16
Shogi.Database/Session/Tables/Session.sql
Normal file
16
Shogi.Database/Session/Tables/Session.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE [session].[Session]
|
||||||
|
(
|
||||||
|
Id BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||||
|
[Name] [session].[SessionName] NOT NULL UNIQUE,
|
||||||
|
Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(),
|
||||||
|
GameOver BIT NOT NULL DEFAULT 0,
|
||||||
|
Player1Id BIGINT NOT NULL,
|
||||||
|
Player2Id BIGINT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id)
|
||||||
|
ON DELETE NO ACTION
|
||||||
|
ON UPDATE NO ACTION
|
||||||
|
)
|
||||||
2
Shogi.Database/Session/Types/JsonDocument.sql
Normal file
2
Shogi.Database/Session/Types/JsonDocument.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE TYPE [session].[JsonDocument]
|
||||||
|
FROM NVARCHAR(max) NOT NULL
|
||||||
2
Shogi.Database/Session/Types/SessionName.sql
Normal file
2
Shogi.Database/Session/Types/SessionName.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE TYPE [session].[SessionName]
|
||||||
|
FROM nvarchar(50) NOT NULL
|
||||||
91
Shogi.Database/Shogi.Database.sqlproj
Normal file
91
Shogi.Database/Shogi.Database.sqlproj
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<Name>Shogi.Database</Name>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
<ProjectVersion>4.1</ProjectVersion>
|
||||||
|
<ProjectGuid>{9b115b71-088f-41ef-858f-c7b155271a9f}</ProjectGuid>
|
||||||
|
<DSP>Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider</DSP>
|
||||||
|
<OutputType>Database</OutputType>
|
||||||
|
<RootPath>
|
||||||
|
</RootPath>
|
||||||
|
<RootNamespace>Shogi.Database</RootNamespace>
|
||||||
|
<AssemblyName>Shogi.Database</AssemblyName>
|
||||||
|
<ModelCollation>1033, CI</ModelCollation>
|
||||||
|
<DefaultFileStructure>BySchemaAndSchemaType</DefaultFileStructure>
|
||||||
|
<DeployToDatabase>True</DeployToDatabase>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<TargetLanguage>CS</TargetLanguage>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<SqlServerVerification>False</SqlServerVerification>
|
||||||
|
<IncludeCompositeObjects>True</IncludeCompositeObjects>
|
||||||
|
<TargetDatabaseSet>True</TargetDatabaseSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
|
||||||
|
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<DefineDebug>false</DefineDebug>
|
||||||
|
<DefineTrace>true</DefineTrace>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
|
||||||
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<DefineDebug>true</DefineDebug>
|
||||||
|
<DefineTrace>true</DefineTrace>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
|
||||||
|
<!-- Default to the v11.0 targets path if the targets file for the current VS version is not found -->
|
||||||
|
<SSDTExists Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets')">True</SSDTExists>
|
||||||
|
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">11.0</VisualStudioVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Condition="'$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
||||||
|
<Import Condition="'$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Properties" />
|
||||||
|
<Folder Include="Session" />
|
||||||
|
<Folder Include="User" />
|
||||||
|
<Folder Include="Session\Tables" />
|
||||||
|
<Folder Include="Session\Stored Procedures" />
|
||||||
|
<Folder Include="User\Tables" />
|
||||||
|
<Folder Include="Session\Types" />
|
||||||
|
<Folder Include="User\Types" />
|
||||||
|
<Folder Include="User\StoredProcedures" />
|
||||||
|
<Folder Include="Post Deployment" />
|
||||||
|
<Folder Include="Post Deployment\Scripts" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Build Include="Session\session.sql" />
|
||||||
|
<Build Include="User\user.sql" />
|
||||||
|
<Build Include="Session\Tables\Session.sql" />
|
||||||
|
<Build Include="Session\Tables\BoardState.sql" />
|
||||||
|
<Build Include="Session\Stored Procedures\CreateSession.sql" />
|
||||||
|
<Build Include="Session\Stored Procedures\CreateBoardState.sql" />
|
||||||
|
<Build Include="User\Tables\User.sql" />
|
||||||
|
<Build Include="Session\Types\SessionName.sql" />
|
||||||
|
<Build Include="User\Types\UserName.sql" />
|
||||||
|
<Build Include="Session\Types\JsonDocument.sql" />
|
||||||
|
<Build Include="User\StoredProcedures\CreateUser.sql" />
|
||||||
|
<Build Include="Session\Stored Procedures\ReadAllSessionsMetadata.sql" />
|
||||||
|
<Build Include="User\StoredProcedures\ReadUser.sql" />
|
||||||
|
<Build Include="User\Tables\LoginPlatform.sql" />
|
||||||
|
<None Include="Post Deployment\Scripts\PopulateLoginPlatforms.sql" />
|
||||||
|
<Build Include="Session\Stored Procedures\UpdateSession.sql" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PostDeploy Include="Post Deployment\Script.PostDeployment.sql" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
14
Shogi.Database/User/StoredProcedures/CreateUser.sql
Normal file
14
Shogi.Database/User/StoredProcedures/CreateUser.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE PROCEDURE [user].[CreateUser]
|
||||||
|
@Name [user].[UserName],
|
||||||
|
@DisplayName NVARCHAR(100),
|
||||||
|
@Platform NVARCHAR(20)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [user].[User] ([Name], DisplayName, [Platform])
|
||||||
|
VALUES
|
||||||
|
(@Name, @DisplayName, @Platform);
|
||||||
|
|
||||||
|
END
|
||||||
11
Shogi.Database/User/StoredProcedures/ReadUser.sql
Normal file
11
Shogi.Database/User/StoredProcedures/ReadUser.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE PROCEDURE [user].[ReadUser]
|
||||||
|
@Name [user].[UserName]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
[Name] as Id,
|
||||||
|
DisplayName,
|
||||||
|
[Platform]
|
||||||
|
FROM [user].[User]
|
||||||
|
WHERE [Name] = @Name;
|
||||||
|
END
|
||||||
4
Shogi.Database/User/Tables/LoginPlatform.sql
Normal file
4
Shogi.Database/User/Tables/LoginPlatform.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE [user].[LoginPlatform]
|
||||||
|
(
|
||||||
|
[Platform] NVARCHAR(20) NOT NULL PRIMARY KEY
|
||||||
|
)
|
||||||
11
Shogi.Database/User/Tables/User.sql
Normal file
11
Shogi.Database/User/Tables/User.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE [user].[User]
|
||||||
|
(
|
||||||
|
[Id] BIGINT NOT NULL PRIMARY KEY IDENTITY,
|
||||||
|
[Name] [user].[UserName] NOT NULL UNIQUE,
|
||||||
|
[DisplayName] NVARCHAR(100) NOT NULL,
|
||||||
|
[Platform] NVARCHAR(20) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT User_Platform FOREIGN KEY ([Platform]) References [user].[LoginPlatform] ([Platform])
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
)
|
||||||
2
Shogi.Database/User/Types/UserName.sql
Normal file
2
Shogi.Database/User/Types/UserName.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE TYPE [user].[UserName]
|
||||||
|
FROM nvarchar(100) NOT NULL
|
||||||
1
Shogi.Database/User/User.sql
Normal file
1
Shogi.Database/User/User.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE SCHEMA [user]
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using Shogi.Domain.Pathing;
|
|
||||||
using Shogi.Domain.Pieces;
|
|
||||||
using System.Numerics;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.UnitTests
|
|
||||||
{
|
|
||||||
public class RookShould
|
|
||||||
{
|
|
||||||
public class MoveSet
|
|
||||||
{
|
|
||||||
private readonly Rook rook1;
|
|
||||||
private readonly Rook rook2;
|
|
||||||
|
|
||||||
public MoveSet()
|
|
||||||
{
|
|
||||||
this.rook1 = new Rook(WhichPlayer.Player1);
|
|
||||||
this.rook2 = new Rook(WhichPlayer.Player2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Player1_HasCorrectMoveSet()
|
|
||||||
{
|
|
||||||
var moveSet = rook1.MoveSet;
|
|
||||||
moveSet.Should().HaveCount(4);
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Player1_Promoted_HasCorrectMoveSet()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
rook1.Promote();
|
|
||||||
rook1.IsPromoted.Should().BeTrue();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var moveSet = rook1.MoveSet;
|
|
||||||
moveSet.Should().HaveCount(8);
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Player2_HasCorrectMoveSet()
|
|
||||||
{
|
|
||||||
var moveSet = rook2.MoveSet;
|
|
||||||
moveSet.Should().HaveCount(4);
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Player2_Promoted_HasCorrectMoveSet()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
rook2.Promote();
|
|
||||||
rook2.IsPromoted.Should().BeTrue();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var moveSet = rook2.MoveSet;
|
|
||||||
moveSet.Should().HaveCount(8);
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep));
|
|
||||||
moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
private readonly Rook rookPlayer1;
|
|
||||||
private readonly Rook rookPlayer2;
|
|
||||||
|
|
||||||
public RookShould()
|
|
||||||
{
|
|
||||||
this.rookPlayer1 = new Rook(WhichPlayer.Player1);
|
|
||||||
this.rookPlayer2 = new Rook(WhichPlayer.Player2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Promote()
|
|
||||||
{
|
|
||||||
this.rookPlayer1.IsPromoted.Should().BeFalse();
|
|
||||||
this.rookPlayer1.CanPromote.Should().BeTrue();
|
|
||||||
this.rookPlayer1.Promote();
|
|
||||||
this.rookPlayer1.IsPromoted.Should().BeTrue();
|
|
||||||
this.rookPlayer1.CanPromote.Should().BeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStepsFromStartToEnd_Player1NotPromoted_LateralMove()
|
|
||||||
{
|
|
||||||
Vector2 start = new(0, 0);
|
|
||||||
Vector2 end = new(0, 5);
|
|
||||||
|
|
||||||
var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
|
|
||||||
|
|
||||||
rookPlayer1.IsPromoted.Should().BeFalse();
|
|
||||||
steps.Should().HaveCount(5);
|
|
||||||
steps.Should().Contain(new Vector2(0, 1));
|
|
||||||
steps.Should().Contain(new Vector2(0, 2));
|
|
||||||
steps.Should().Contain(new Vector2(0, 3));
|
|
||||||
steps.Should().Contain(new Vector2(0, 4));
|
|
||||||
steps.Should().Contain(new Vector2(0, 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStepsFromStartToEnd_Player1NotPromoted_DiagonalMove()
|
|
||||||
{
|
|
||||||
Vector2 start = new(0, 0);
|
|
||||||
Vector2 end = new(1, 1);
|
|
||||||
|
|
||||||
var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
|
|
||||||
|
|
||||||
rookPlayer1.IsPromoted.Should().BeFalse();
|
|
||||||
steps.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStepsFromStartToEnd_Player1Promoted_LateralMove()
|
|
||||||
{
|
|
||||||
Vector2 start = new(0, 0);
|
|
||||||
Vector2 end = new(0, 5);
|
|
||||||
rookPlayer1.Promote();
|
|
||||||
|
|
||||||
var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
|
|
||||||
|
|
||||||
rookPlayer1.IsPromoted.Should().BeTrue();
|
|
||||||
steps.Should().HaveCount(5);
|
|
||||||
steps.Should().Contain(new Vector2(0, 1));
|
|
||||||
steps.Should().Contain(new Vector2(0, 2));
|
|
||||||
steps.Should().Contain(new Vector2(0, 3));
|
|
||||||
steps.Should().Contain(new Vector2(0, 4));
|
|
||||||
steps.Should().Contain(new Vector2(0, 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStepsFromStartToEnd_Player1Promoted_DiagonalMove()
|
|
||||||
{
|
|
||||||
Vector2 start = new(0, 0);
|
|
||||||
Vector2 end = new(1, 1);
|
|
||||||
rookPlayer1.Promote();
|
|
||||||
|
|
||||||
var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
|
|
||||||
|
|
||||||
rookPlayer1.IsPromoted.Should().BeTrue();
|
|
||||||
steps.Should().HaveCount(1);
|
|
||||||
steps.Should().Contain(new Vector2(1, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStepsFromStartToEnd_Player2NotPromoted_LateralMove()
|
|
||||||
{
|
|
||||||
Vector2 start = new(0, 0);
|
|
||||||
Vector2 end = new(0, 5);
|
|
||||||
|
|
||||||
var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
|
|
||||||
|
|
||||||
rookPlayer1.IsPromoted.Should().BeFalse();
|
|
||||||
steps.Should().HaveCount(5);
|
|
||||||
steps.Should().Contain(new Vector2(0, 1));
|
|
||||||
steps.Should().Contain(new Vector2(0, 2));
|
|
||||||
steps.Should().Contain(new Vector2(0, 3));
|
|
||||||
steps.Should().Contain(new Vector2(0, 4));
|
|
||||||
steps.Should().Contain(new Vector2(0, 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStepsFromStartToEnd_Player2NotPromoted_DiagonalMove()
|
|
||||||
{
|
|
||||||
Vector2 start = new(0, 0);
|
|
||||||
Vector2 end = new(1, 1);
|
|
||||||
|
|
||||||
var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
|
|
||||||
|
|
||||||
rookPlayer1.IsPromoted.Should().BeFalse();
|
|
||||||
steps.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStepsFromStartToEnd_Player2Promoted_LateralMove()
|
|
||||||
{
|
|
||||||
Vector2 start = new(0, 0);
|
|
||||||
Vector2 end = new(0, 5);
|
|
||||||
rookPlayer1.Promote();
|
|
||||||
|
|
||||||
var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
|
|
||||||
|
|
||||||
rookPlayer1.IsPromoted.Should().BeTrue();
|
|
||||||
steps.Should().HaveCount(5);
|
|
||||||
steps.Should().Contain(new Vector2(0, 1));
|
|
||||||
steps.Should().Contain(new Vector2(0, 2));
|
|
||||||
steps.Should().Contain(new Vector2(0, 3));
|
|
||||||
steps.Should().Contain(new Vector2(0, 4));
|
|
||||||
steps.Should().Contain(new Vector2(0, 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStepsFromStartToEnd_Player2Promoted_DiagonalMove()
|
|
||||||
{
|
|
||||||
Vector2 start = new(0, 0);
|
|
||||||
Vector2 end = new(1, 1);
|
|
||||||
rookPlayer1.Promote();
|
|
||||||
|
|
||||||
var steps = rookPlayer1.GetPathFromStartToEnd(start, end);
|
|
||||||
|
|
||||||
rookPlayer1.IsPromoted.Should().BeTrue();
|
|
||||||
steps.Should().HaveCount(1);
|
|
||||||
steps.Should().Contain(new Vector2(1, 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="FluentAssertions" Version="6.2.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.4.2-pre.12" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Shogi.Domain\Shogi.Domain.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using FluentAssertions.Execution;
|
|
||||||
using System;
|
|
||||||
using Xunit;
|
|
||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.UnitTests
|
|
||||||
{
|
|
||||||
public class ShogiShould
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper console;
|
|
||||||
public ShogiShould(ITestOutputHelper console)
|
|
||||||
{
|
|
||||||
this.console = console;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MoveAPieceToAnEmptyPosition()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
board["A4"].Should().BeNull();
|
|
||||||
var expectedPiece = board["A3"];
|
|
||||||
expectedPiece.Should().NotBeNull();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
shogi.Move("A3", "A4", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
board["A3"].Should().BeNull();
|
|
||||||
board["A4"].Should().Be(expectedPiece);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AllowValidMoves_AfterCheck()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
// P1 Pawn
|
|
||||||
shogi.Move("C3", "C4", false);
|
|
||||||
// P2 Pawn
|
|
||||||
shogi.Move("G7", "G6", false);
|
|
||||||
// P1 Bishop puts P2 in check
|
|
||||||
shogi.Move("B2", "G7", false);
|
|
||||||
board.InCheck.Should().Be(WhichPlayer.Player2);
|
|
||||||
|
|
||||||
// Act - P2 is able to un-check theirself.
|
|
||||||
/// P2 King moves out of check
|
|
||||||
shogi.Move("E9", "E8", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
board.InCheck.Should().BeNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_MoveFromEmptyPosition()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
board["D5"].Should().BeNull();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var act = () => shogi.Move("D5", "D6", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["D5"].Should().BeNull();
|
|
||||||
board["D6"].Should().BeNull();
|
|
||||||
board.Player1Hand.Should().BeEmpty();
|
|
||||||
board.Player2Hand.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_MoveToCurrentPosition()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
var expectedPiece = board["A3"];
|
|
||||||
|
|
||||||
// Act - P1 "moves" pawn to the position it already exists at.
|
|
||||||
var act = () => shogi.Move("A3", "A3", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["A3"].Should().Be(expectedPiece);
|
|
||||||
board.Player1Hand.Should().BeEmpty();
|
|
||||||
board.Player2Hand.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_MoveSet()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
var expectedPiece = board["A1"];
|
|
||||||
expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
|
|
||||||
// Act - Move Lance illegally
|
|
||||||
var act = () => shogi.Move("A1", "D5", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["A1"].Should().Be(expectedPiece);
|
|
||||||
board["A5"].Should().BeNull();
|
|
||||||
board.Player1Hand.Should().BeEmpty();
|
|
||||||
board.Player2Hand.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_Ownership()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
var expectedPiece = board["A7"];
|
|
||||||
expectedPiece!.Owner.Should().Be(WhichPlayer.Player2);
|
|
||||||
board.WhoseTurn.Should().Be(WhichPlayer.Player1);
|
|
||||||
|
|
||||||
// Act - Move Player2 Pawn when it is Player1 turn.
|
|
||||||
var act = () => shogi.Move("A7", "A6", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["A7"].Should().Be(expectedPiece);
|
|
||||||
board["A6"].Should().BeNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_MoveThroughAllies()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
var lance = board["A1"];
|
|
||||||
var pawn = board["A3"];
|
|
||||||
lance!.Owner.Should().Be(pawn!.Owner);
|
|
||||||
|
|
||||||
// Act - Move P1 Lance through P1 Pawn.
|
|
||||||
var act = () => shogi.Move("A1", "A5", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["A1"].Should().Be(lance);
|
|
||||||
board["A3"].Should().Be(pawn);
|
|
||||||
board["A5"].Should().BeNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_CaptureAlly()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
var knight = board["B1"];
|
|
||||||
var pawn = board["C3"];
|
|
||||||
knight!.Owner.Should().Be(pawn!.Owner);
|
|
||||||
|
|
||||||
// Act - P1 Knight tries to capture P1 Pawn.
|
|
||||||
var act = () => shogi.Move("B1", "C3", false);
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["B1"].Should().Be(knight);
|
|
||||||
board["C3"].Should().Be(pawn);
|
|
||||||
board.Player1Hand.Should().BeEmpty();
|
|
||||||
board.Player2Hand.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_Check()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
// P1 Pawn
|
|
||||||
shogi.Move("C3", "C4", false);
|
|
||||||
// P2 Pawn
|
|
||||||
shogi.Move("G7", "G6", false);
|
|
||||||
// P1 Bishop puts P2 in check
|
|
||||||
shogi.Move("B2", "G7", false);
|
|
||||||
board.InCheck.Should().Be(WhichPlayer.Player2);
|
|
||||||
var lance = board["I9"];
|
|
||||||
|
|
||||||
// Act - P2 moves Lance while in check.
|
|
||||||
var act = () => shogi.Move("I9", "I8", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board.InCheck.Should().Be(WhichPlayer.Player2);
|
|
||||||
board["I9"].Should().Be(lance);
|
|
||||||
board["I8"].Should().BeNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
// TODO: Consider nesting classes to share this setup in a constructor but have act and assert as separate facts.
|
|
||||||
public void PreventInvalidDrops_MoveSet()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var board = new BoardState();
|
|
||||||
var shogi = new Session(board);
|
|
||||||
// P1 Pawn
|
|
||||||
shogi.Move("C3", "C4", false);
|
|
||||||
// P2 Pawn
|
|
||||||
shogi.Move("I7", "I6", false);
|
|
||||||
// P1 Bishop takes P2 Pawn.
|
|
||||||
shogi.Move("B2", "G7", false);
|
|
||||||
// P2 Gold, block check from P1 Bishop.
|
|
||||||
shogi.Move("F9", "F8", false);
|
|
||||||
// P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance
|
|
||||||
shogi.Move("G7", "H8", true);
|
|
||||||
// P2 Pawn again
|
|
||||||
shogi.Move("I6", "I5", false);
|
|
||||||
// P1 Bishop takes P2 Knight
|
|
||||||
shogi.Move("H8", "H9", false);
|
|
||||||
// P2 Pawn again
|
|
||||||
shogi.Move("I5", "I4", false);
|
|
||||||
// P1 Bishop takes P2 Lance
|
|
||||||
shogi.Move("H9", "I9", false);
|
|
||||||
// P2 Pawn captures P1 Pawn
|
|
||||||
shogi.Move("I4", "I3", false);
|
|
||||||
board.Player1Hand.Count.Should().Be(4);
|
|
||||||
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
|
|
||||||
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
|
|
||||||
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
|
|
||||||
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
|
|
||||||
board.WhoseTurn.Should().Be(WhichPlayer.Player1);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally placing Knight from the hand in farthest rank.
|
|
||||||
board["H9"].Should().BeNull();
|
|
||||||
var act = () => shogi.Move(WhichPiece.Knight, "H9");
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["H9"].Should().BeNull();
|
|
||||||
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally placing Knight from the hand in second farthest row.
|
|
||||||
board["H8"].Should().BeNull();
|
|
||||||
act = () => shogi.Move(WhichPiece.Knight, "H8");
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["H8"].Should().BeNull();
|
|
||||||
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally place Lance from the hand.
|
|
||||||
board["H9"].Should().BeNull();
|
|
||||||
act = () => shogi.Move(WhichPiece.Knight, "H9");
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["H9"].Should().BeNull();
|
|
||||||
board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally place Pawn from the hand.
|
|
||||||
board["H9"].Should().BeNull();
|
|
||||||
act = () => shogi.Move(WhichPiece.Pawn, "H9");
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
|
||||||
board["H9"].Should().BeNull();
|
|
||||||
board.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);
|
|
||||||
// boardState["E5"].Should().BeNull();
|
|
||||||
|
|
||||||
// // Act - P2 places a Bishop while in check.
|
|
||||||
// var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5"));
|
|
||||||
|
|
||||||
// // Assert
|
|
||||||
// dropSuccess.Should().BeFalse();
|
|
||||||
// boardState["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);
|
|
||||||
// boardState["I9"].Should().NotBeNull();
|
|
||||||
// boardState["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
// boardState["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);
|
|
||||||
// boardState["I9"].Should().NotBeNull();
|
|
||||||
// boardState["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
// boardState["I9"].Owner.Should().Be(WhichPlayer.Player2);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Check()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var boardState = new BoardState();
|
|
||||||
var shogi = new Session(boardState);
|
|
||||||
// P1 Pawn
|
|
||||||
shogi.Move("C3", "C4", false);
|
|
||||||
// P2 Pawn
|
|
||||||
shogi.Move("G7", "G6", false);
|
|
||||||
|
|
||||||
// Act - P1 Bishop, check
|
|
||||||
shogi.Move("B2", "G7", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
boardState.InCheck.Should().Be(WhichPlayer.Player2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Promote()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var boardState = new BoardState();
|
|
||||||
var shogi = new Session(boardState);
|
|
||||||
// P1 Pawn
|
|
||||||
shogi.Move("C3", "C4", false);
|
|
||||||
// P2 Pawn
|
|
||||||
shogi.Move("G7", "G6", false);
|
|
||||||
|
|
||||||
// Act - P1 moves across promote threshold.
|
|
||||||
shogi.Move("B2", "G7", true);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
boardState["B2"].Should().BeNull();
|
|
||||||
boardState["G7"].Should().NotBeNull();
|
|
||||||
boardState["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop);
|
|
||||||
boardState["G7"]!.Owner.Should().Be(WhichPlayer.Player1);
|
|
||||||
boardState["G7"]!.IsPromoted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Capture()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var boardState = new BoardState();
|
|
||||||
var shogi = new Session(boardState);
|
|
||||||
var p1Bishop = boardState["B2"];
|
|
||||||
p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop);
|
|
||||||
shogi.Move("C3", "C4", false);
|
|
||||||
shogi.Move("G7", "G6", false);
|
|
||||||
|
|
||||||
// Act - P1 Bishop captures P2 Bishop
|
|
||||||
shogi.Move("B2", "H8", false);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
boardState["B2"].Should().BeNull();
|
|
||||||
boardState["H8"].Should().Be(p1Bishop);
|
|
||||||
|
|
||||||
boardState
|
|
||||||
.Player1Hand
|
|
||||||
.Should()
|
|
||||||
.ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CheckMate()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var boardState = new BoardState();
|
|
||||||
var shogi = new Session(boardState);
|
|
||||||
// P1 Rook
|
|
||||||
shogi.Move("H2", "E2", false);
|
|
||||||
// P2 Gold
|
|
||||||
shogi.Move("F9", "G8", false);
|
|
||||||
// P1 Pawn
|
|
||||||
shogi.Move("E3", "E4", false);
|
|
||||||
// P2 other Gold
|
|
||||||
shogi.Move("D9", "C8", false);
|
|
||||||
// P1 same Pawn
|
|
||||||
shogi.Move("E4", "E5", false);
|
|
||||||
// P2 Pawn
|
|
||||||
shogi.Move("E7", "E6", false);
|
|
||||||
// P1 Pawn takes P2 Pawn
|
|
||||||
shogi.Move("E5", "E6", false);
|
|
||||||
// P2 King
|
|
||||||
shogi.Move("E9", "E8", false);
|
|
||||||
// P1 Pawn promotes; threatens P2 King
|
|
||||||
shogi.Move("E6", "E7", true);
|
|
||||||
// P2 King retreat
|
|
||||||
shogi.Move("E8", "E9", false);
|
|
||||||
|
|
||||||
// Act - P1 Pawn wins by checkmate.
|
|
||||||
shogi.Move("E7", "E8", false);
|
|
||||||
|
|
||||||
// Assert - checkmate
|
|
||||||
console.WriteLine(shogi.ToStringStateAsAscii());
|
|
||||||
boardState.IsCheckmate.Should().BeTrue();
|
|
||||||
boardState.InCheck.Should().Be(WhichPlayer.Player2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
242
Shogi.Domain/Aggregates/Session.cs
Normal file
242
Shogi.Domain/Aggregates/Session.cs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
using Shogi.Domain.ValueObjects;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Shogi.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Facilitates Shogi board state transitions, cognisant of Shogi rules.
|
||||||
|
/// The board is always from Player1's perspective.
|
||||||
|
/// [0,0] is the lower-left position, [8,8] is the higher-right position
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Session
|
||||||
|
{
|
||||||
|
private readonly StandardRules rules;
|
||||||
|
|
||||||
|
public Session(string name, BoardState initialState, string player1, string? player2 = null)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Player1 = player1;
|
||||||
|
Player2 = player2;
|
||||||
|
BoardState = initialState;
|
||||||
|
rules = new StandardRules(BoardState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoardState BoardState { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string Player1 { get; }
|
||||||
|
public string? Player2 { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The strategy involves simulating a move on a throw-away board state that can be used to
|
||||||
|
/// validate legal vs illegal moves without having to worry about reverting board state.
|
||||||
|
/// </remarks>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public void Move(string from, string to, bool isPromotion)
|
||||||
|
{
|
||||||
|
var simulationState = new BoardState(BoardState);
|
||||||
|
var simulation = new StandardRules(simulationState);
|
||||||
|
var moveResult = simulation.Move(from, to, isPromotion);
|
||||||
|
if (!moveResult.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(moveResult.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already in check, assert the move that resulted in check no longer results in check.
|
||||||
|
if (BoardState.InCheck == BoardState.WhoseTurn
|
||||||
|
&& simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMoveTo))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unable to move because you are still in check.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||||
|
if (simulation.IsPlayerInCheckAfterMove())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Illegal move. This move places you in check.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = rules.Move(from, to, isPromotion);
|
||||||
|
if (rules.IsOpponentInCheckAfterMove())
|
||||||
|
{
|
||||||
|
BoardState.InCheck = otherPlayer;
|
||||||
|
if (rules.IsOpponentInCheckMate())
|
||||||
|
{
|
||||||
|
BoardState.IsCheckmate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BoardState.InCheck = null;
|
||||||
|
}
|
||||||
|
BoardState.WhoseTurn = otherPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Move(WhichPiece pieceInHand, string to)
|
||||||
|
{
|
||||||
|
var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{pieceInHand} does not exist in the hand.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BoardState[to] != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var toVector = Notation.FromBoardNotation(to);
|
||||||
|
switch (pieceInHand)
|
||||||
|
{
|
||||||
|
case WhichPiece.Knight:
|
||||||
|
{
|
||||||
|
// Knight cannot be placed onto the farthest two ranks from the hand.
|
||||||
|
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6
|
||||||
|
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WhichPiece.Lance:
|
||||||
|
case WhichPiece.Pawn:
|
||||||
|
{
|
||||||
|
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
|
||||||
|
if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8
|
||||||
|
|| BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempBoard = new BoardState(BoardState);
|
||||||
|
var simulation = new StandardRules(tempBoard);
|
||||||
|
var moveResult = simulation.Move(pieceInHand, to);
|
||||||
|
if (!moveResult.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(moveResult.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||||
|
if (BoardState.InCheck == BoardState.WhoseTurn)
|
||||||
|
{
|
||||||
|
//if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn))
|
||||||
|
//{
|
||||||
|
// throw new InvalidOperationException("Illegal move. You're still in check!");
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition;
|
||||||
|
//if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer))
|
||||||
|
//{
|
||||||
|
|
||||||
|
//}
|
||||||
|
|
||||||
|
//rules.Move(from, to, isPromotion);
|
||||||
|
//if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer))
|
||||||
|
//{
|
||||||
|
// board.InCheck = otherPlayer;
|
||||||
|
// board.IsCheckmate = rules.EvaluateCheckmate();
|
||||||
|
//}
|
||||||
|
//else
|
||||||
|
//{
|
||||||
|
// board.InCheck = null;
|
||||||
|
//}
|
||||||
|
BoardState.WhoseTurn = otherPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prints a ASCII representation of the board for debugging board state.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string ToStringStateAsAscii()
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append(" ");
|
||||||
|
builder.Append("Player 2(.)");
|
||||||
|
builder.AppendLine();
|
||||||
|
for (var rank = 8; rank >= 0; rank--)
|
||||||
|
{
|
||||||
|
// Horizontal line
|
||||||
|
builder.Append(" - ");
|
||||||
|
for (var file = 0; file < 8; file++) builder.Append("- - ");
|
||||||
|
builder.Append("- -");
|
||||||
|
|
||||||
|
// Print Rank ruler.
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.Append($"{rank + 1} ");
|
||||||
|
|
||||||
|
// Print pieces.
|
||||||
|
builder.Append(" |");
|
||||||
|
for (var x = 0; x < 9; x++)
|
||||||
|
{
|
||||||
|
var piece = BoardState[x, rank];
|
||||||
|
if (piece == null)
|
||||||
|
{
|
||||||
|
builder.Append(" ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.AppendFormat("{0}", ToAscii(piece));
|
||||||
|
}
|
||||||
|
builder.Append('|');
|
||||||
|
}
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal line
|
||||||
|
builder.Append(" - ");
|
||||||
|
for (var x = 0; x < 8; x++) builder.Append("- - ");
|
||||||
|
builder.Append("- -");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.Append(" ");
|
||||||
|
builder.Append("Player 1");
|
||||||
|
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine();
|
||||||
|
// Print File ruler.
|
||||||
|
builder.Append(" ");
|
||||||
|
builder.Append(" A B C D E F G H I ");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="piece"></param>
|
||||||
|
/// <returns>
|
||||||
|
/// A string with three characters.
|
||||||
|
/// The first character indicates promotion status.
|
||||||
|
/// The second character indicates piece.
|
||||||
|
/// The third character indicates ownership.
|
||||||
|
/// </returns>
|
||||||
|
private static string ToAscii(Piece piece)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
if (piece.IsPromoted) builder.Append('^');
|
||||||
|
else builder.Append(' ');
|
||||||
|
|
||||||
|
var name = piece.WhichPiece switch
|
||||||
|
{
|
||||||
|
WhichPiece.King => "K",
|
||||||
|
WhichPiece.GoldGeneral => "G",
|
||||||
|
WhichPiece.SilverGeneral => "S",
|
||||||
|
WhichPiece.Bishop => "B",
|
||||||
|
WhichPiece.Rook => "R",
|
||||||
|
WhichPiece.Knight => "k",
|
||||||
|
WhichPiece.Lance => "L",
|
||||||
|
WhichPiece.Pawn => "P",
|
||||||
|
_ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."),
|
||||||
|
};
|
||||||
|
builder.Append(name);
|
||||||
|
|
||||||
|
if (piece.Owner == WhichPlayer.Player2) builder.Append('.');
|
||||||
|
else builder.Append(' ');
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,246 +1,251 @@
|
|||||||
using Shogi.Domain.Pieces;
|
using Shogi.Domain.ValueObjects;
|
||||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.Pieces.Piece>;
|
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
||||||
|
|
||||||
namespace Shogi.Domain
|
namespace Shogi.Domain
|
||||||
{
|
{
|
||||||
public class BoardState
|
public class BoardState
|
||||||
{
|
{
|
||||||
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
/// <summary>
|
||||||
/// <summary>
|
/// Board state before any moves have been made, using standard setup and rules.
|
||||||
/// Key is position notation, such as "E4".
|
/// </summary>
|
||||||
/// </summary>
|
public static readonly BoardState StandardStarting = new();
|
||||||
private readonly Dictionary<string, Piece?> board;
|
|
||||||
|
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
||||||
|
/// <summary>
|
||||||
|
/// Key is position notation, such as "E4".
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, Piece?> board;
|
||||||
|
|
||||||
|
|
||||||
public BoardState(Dictionary<string, Piece?> state)
|
public BoardState(Dictionary<string, Piece?> state)
|
||||||
{
|
{
|
||||||
board = state;
|
board = state;
|
||||||
Player1Hand = new List<Piece>();
|
Player1Hand = new List<Piece>();
|
||||||
Player2Hand = new List<Piece>();
|
Player2Hand = new List<Piece>();
|
||||||
PreviousMoveTo = Vector2.Zero;
|
PreviousMoveTo = Vector2.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BoardState()
|
public BoardState()
|
||||||
{
|
{
|
||||||
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
||||||
InitializeBoardState();
|
InitializeBoardState();
|
||||||
Player1Hand = new List<Piece>();
|
Player1Hand = new List<Piece>();
|
||||||
Player2Hand = new List<Piece>();
|
Player2Hand = new List<Piece>();
|
||||||
PreviousMoveTo = Vector2.Zero;
|
PreviousMoveTo = Vector2.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, Piece?> State => board;
|
public Dictionary<string, Piece?> State => board;
|
||||||
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||||
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||||
{
|
{
|
||||||
var piece = kvp.Value;
|
var piece = kvp.Value;
|
||||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1;
|
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1;
|
||||||
}).Key);
|
}).Key);
|
||||||
public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||||
{
|
{
|
||||||
var piece = kvp.Value;
|
var piece = kvp.Value;
|
||||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2;
|
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2;
|
||||||
}).Key);
|
}).Key);
|
||||||
public List<Piece> Player1Hand { get; }
|
public List<Piece> Player1Hand { get; }
|
||||||
public List<Piece> Player2Hand { get; }
|
public List<Piece> Player2Hand { get; }
|
||||||
public Vector2 PreviousMoveFrom { get; private set; }
|
public Vector2 PreviousMoveFrom { get; private set; }
|
||||||
public Vector2 PreviousMoveTo { get; private set; }
|
public Vector2 PreviousMoveTo { get; private set; }
|
||||||
public WhichPlayer WhoseTurn { get; set; }
|
public WhichPlayer WhoseTurn { get; set; }
|
||||||
public WhichPlayer? InCheck { get; set; }
|
public WhichPlayer? InCheck { get; set; }
|
||||||
public bool IsCheckmate { get; set; }
|
public bool IsCheckmate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy constructor.
|
/// Copy constructor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BoardState(BoardState other) : this()
|
public BoardState(BoardState other) : this()
|
||||||
{
|
{
|
||||||
foreach (var kvp in other.board)
|
foreach (var kvp in other.board)
|
||||||
{
|
{
|
||||||
// Replace copy constructor with static factory method in Piece.cs
|
// Replace copy constructor with static factory method in Piece.cs
|
||||||
board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value);
|
board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value);
|
||||||
}
|
}
|
||||||
WhoseTurn = other.WhoseTurn;
|
WhoseTurn = other.WhoseTurn;
|
||||||
InCheck = other.InCheck;
|
InCheck = other.InCheck;
|
||||||
IsCheckmate = other.IsCheckmate;
|
IsCheckmate = other.IsCheckmate;
|
||||||
PreviousMoveTo = other.PreviousMoveTo;
|
PreviousMoveTo = other.PreviousMoveTo;
|
||||||
Player1Hand.AddRange(other.Player1Hand);
|
Player1Hand.AddRange(other.Player1Hand);
|
||||||
Player2Hand.AddRange(other.Player2Hand);
|
Player2Hand.AddRange(other.Player2Hand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Piece? this[string notation]
|
public Piece? this[string notation]
|
||||||
{
|
{
|
||||||
// TODO: Validate "notation" here and throw an exception if invalid.
|
// TODO: Validate "notation" here and throw an exception if invalid.
|
||||||
get => board[notation];
|
get => board[notation];
|
||||||
set => board[notation] = value;
|
set => board[notation] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Piece? this[Vector2 vector]
|
public Piece? this[Vector2 vector]
|
||||||
{
|
{
|
||||||
get => this[Notation.ToBoardNotation(vector)];
|
get => this[Notation.ToBoardNotation(vector)];
|
||||||
set => this[Notation.ToBoardNotation(vector)] = value;
|
set => this[Notation.ToBoardNotation(vector)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Piece? this[int x, int y]
|
public Piece? this[int x, int y]
|
||||||
{
|
{
|
||||||
get => this[Notation.ToBoardNotation(x, y)];
|
get => this[Notation.ToBoardNotation(x, y)];
|
||||||
set => this[Notation.ToBoardNotation(x, y)] = value;
|
set => this[Notation.ToBoardNotation(x, y)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
|
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
|
||||||
{
|
{
|
||||||
PreviousMoveFrom = from;
|
PreviousMoveFrom = from;
|
||||||
PreviousMoveTo = to;
|
PreviousMoveTo = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true if the given path can be traversed without colliding into a piece.
|
/// Returns true if the given path can be traversed without colliding into a piece.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPathBlocked(IEnumerable<Vector2> path)
|
public bool IsPathBlocked(IEnumerable<Vector2> path)
|
||||||
{
|
{
|
||||||
return !path.Any()
|
return !path.Any()
|
||||||
|| path.SkipLast(1).Any(position => this[position] != null)
|
|| path.SkipLast(1).Any(position => this[position] != null)
|
||||||
|| this[path.Last()]?.Owner == WhoseTurn;
|
|| this[path.Last()]?.Owner == WhoseTurn;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool IsWithinPromotionZone(Vector2 position)
|
internal bool IsWithinPromotionZone(Vector2 position)
|
||||||
{
|
{
|
||||||
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|
||||||
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
|
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsWithinBoardBoundary(Vector2 position)
|
internal static bool IsWithinBoardBoundary(Vector2 position)
|
||||||
{
|
{
|
||||||
return position.X <= 8 && position.X >= 0
|
return position.X <= 8 && position.X >= 0
|
||||||
&& position.Y <= 8 && position.Y >= 0;
|
&& position.Y <= 8 && position.Y >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
||||||
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
||||||
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
internal void Capture(Vector2 to)
|
internal void Capture(Vector2 to)
|
||||||
{
|
{
|
||||||
var piece = this[to];
|
var piece = this[to];
|
||||||
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
|
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
|
||||||
|
|
||||||
piece.Capture(WhoseTurn);
|
piece.Capture(WhoseTurn);
|
||||||
ActivePlayerHand.Add(piece);
|
ActivePlayerHand.Add(piece);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Does not include the start position.
|
/// Does not include the start position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static IEnumerable<Vector2> GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction)
|
internal static IEnumerable<Vector2> GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction)
|
||||||
{
|
{
|
||||||
var next = start;
|
var next = start;
|
||||||
while (IsWithinBoardBoundary(next + direction))
|
while (IsWithinBoardBoundary(next + direction))
|
||||||
{
|
{
|
||||||
next += direction;
|
next += direction;
|
||||||
yield return next;
|
yield return next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Piece? QueryFirstPieceInPath(IEnumerable<Vector2> path)
|
internal Piece? QueryFirstPieceInPath(IEnumerable<Vector2> path)
|
||||||
{
|
{
|
||||||
foreach (var step in path)
|
foreach (var step in path)
|
||||||
{
|
{
|
||||||
if (this[step] != null) return this[step];
|
if (this[step] != null) return this[step];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeBoardState()
|
private void InitializeBoardState()
|
||||||
{
|
{
|
||||||
this["A1"] = new Lance(WhichPlayer.Player1);
|
this["A1"] = new Lance(WhichPlayer.Player1);
|
||||||
this["B1"] = new Knight(WhichPlayer.Player1);
|
this["B1"] = new Knight(WhichPlayer.Player1);
|
||||||
this["C1"] = new SilverGeneral(WhichPlayer.Player1);
|
this["C1"] = new SilverGeneral(WhichPlayer.Player1);
|
||||||
this["D1"] = new GoldGeneral(WhichPlayer.Player1);
|
this["D1"] = new GoldGeneral(WhichPlayer.Player1);
|
||||||
this["E1"] = new King(WhichPlayer.Player1);
|
this["E1"] = new King(WhichPlayer.Player1);
|
||||||
this["F1"] = new GoldGeneral(WhichPlayer.Player1);
|
this["F1"] = new GoldGeneral(WhichPlayer.Player1);
|
||||||
this["G1"] = new SilverGeneral(WhichPlayer.Player1);
|
this["G1"] = new SilverGeneral(WhichPlayer.Player1);
|
||||||
this["H1"] = new Knight(WhichPlayer.Player1);
|
this["H1"] = new Knight(WhichPlayer.Player1);
|
||||||
this["I1"] = new Lance(WhichPlayer.Player1);
|
this["I1"] = new Lance(WhichPlayer.Player1);
|
||||||
|
|
||||||
this["A2"] = null;
|
this["A2"] = null;
|
||||||
this["B2"] = new Bishop(WhichPlayer.Player1);
|
this["B2"] = new Bishop(WhichPlayer.Player1);
|
||||||
this["C2"] = null;
|
this["C2"] = null;
|
||||||
this["D2"] = null;
|
this["D2"] = null;
|
||||||
this["E2"] = null;
|
this["E2"] = null;
|
||||||
this["F2"] = null;
|
this["F2"] = null;
|
||||||
this["G2"] = null;
|
this["G2"] = null;
|
||||||
this["H2"] = new Rook(WhichPlayer.Player1);
|
this["H2"] = new Rook(WhichPlayer.Player1);
|
||||||
this["I2"] = null;
|
this["I2"] = null;
|
||||||
|
|
||||||
this["A3"] = new Pawn(WhichPlayer.Player1);
|
this["A3"] = new Pawn(WhichPlayer.Player1);
|
||||||
this["B3"] = new Pawn(WhichPlayer.Player1);
|
this["B3"] = new Pawn(WhichPlayer.Player1);
|
||||||
this["C3"] = new Pawn(WhichPlayer.Player1);
|
this["C3"] = new Pawn(WhichPlayer.Player1);
|
||||||
this["D3"] = new Pawn(WhichPlayer.Player1);
|
this["D3"] = new Pawn(WhichPlayer.Player1);
|
||||||
this["E3"] = new Pawn(WhichPlayer.Player1);
|
this["E3"] = new Pawn(WhichPlayer.Player1);
|
||||||
this["F3"] = new Pawn(WhichPlayer.Player1);
|
this["F3"] = new Pawn(WhichPlayer.Player1);
|
||||||
this["G3"] = new Pawn(WhichPlayer.Player1);
|
this["G3"] = new Pawn(WhichPlayer.Player1);
|
||||||
this["H3"] = new Pawn(WhichPlayer.Player1);
|
this["H3"] = new Pawn(WhichPlayer.Player1);
|
||||||
this["I3"] = new Pawn(WhichPlayer.Player1);
|
this["I3"] = new Pawn(WhichPlayer.Player1);
|
||||||
|
|
||||||
this["A4"] = null;
|
this["A4"] = null;
|
||||||
this["B4"] = null;
|
this["B4"] = null;
|
||||||
this["C4"] = null;
|
this["C4"] = null;
|
||||||
this["D4"] = null;
|
this["D4"] = null;
|
||||||
this["E4"] = null;
|
this["E4"] = null;
|
||||||
this["F4"] = null;
|
this["F4"] = null;
|
||||||
this["G4"] = null;
|
this["G4"] = null;
|
||||||
this["H4"] = null;
|
this["H4"] = null;
|
||||||
this["I4"] = null;
|
this["I4"] = null;
|
||||||
|
|
||||||
this["A5"] = null;
|
this["A5"] = null;
|
||||||
this["B5"] = null;
|
this["B5"] = null;
|
||||||
this["C5"] = null;
|
this["C5"] = null;
|
||||||
this["D5"] = null;
|
this["D5"] = null;
|
||||||
this["E5"] = null;
|
this["E5"] = null;
|
||||||
this["F5"] = null;
|
this["F5"] = null;
|
||||||
this["G5"] = null;
|
this["G5"] = null;
|
||||||
this["H5"] = null;
|
this["H5"] = null;
|
||||||
this["I5"] = null;
|
this["I5"] = null;
|
||||||
|
|
||||||
this["A6"] = null;
|
this["A6"] = null;
|
||||||
this["B6"] = null;
|
this["B6"] = null;
|
||||||
this["C6"] = null;
|
this["C6"] = null;
|
||||||
this["D6"] = null;
|
this["D6"] = null;
|
||||||
this["E6"] = null;
|
this["E6"] = null;
|
||||||
this["F6"] = null;
|
this["F6"] = null;
|
||||||
this["G6"] = null;
|
this["G6"] = null;
|
||||||
this["H6"] = null;
|
this["H6"] = null;
|
||||||
this["I6"] = null;
|
this["I6"] = null;
|
||||||
|
|
||||||
this["A7"] = new Pawn(WhichPlayer.Player2);
|
this["A7"] = new Pawn(WhichPlayer.Player2);
|
||||||
this["B7"] = new Pawn(WhichPlayer.Player2);
|
this["B7"] = new Pawn(WhichPlayer.Player2);
|
||||||
this["C7"] = new Pawn(WhichPlayer.Player2);
|
this["C7"] = new Pawn(WhichPlayer.Player2);
|
||||||
this["D7"] = new Pawn(WhichPlayer.Player2);
|
this["D7"] = new Pawn(WhichPlayer.Player2);
|
||||||
this["E7"] = new Pawn(WhichPlayer.Player2);
|
this["E7"] = new Pawn(WhichPlayer.Player2);
|
||||||
this["F7"] = new Pawn(WhichPlayer.Player2);
|
this["F7"] = new Pawn(WhichPlayer.Player2);
|
||||||
this["G7"] = new Pawn(WhichPlayer.Player2);
|
this["G7"] = new Pawn(WhichPlayer.Player2);
|
||||||
this["H7"] = new Pawn(WhichPlayer.Player2);
|
this["H7"] = new Pawn(WhichPlayer.Player2);
|
||||||
this["I7"] = new Pawn(WhichPlayer.Player2);
|
this["I7"] = new Pawn(WhichPlayer.Player2);
|
||||||
|
|
||||||
this["A8"] = null;
|
this["A8"] = null;
|
||||||
this["B8"] = new Rook(WhichPlayer.Player2);
|
this["B8"] = new Rook(WhichPlayer.Player2);
|
||||||
this["C8"] = null;
|
this["C8"] = null;
|
||||||
this["D8"] = null;
|
this["D8"] = null;
|
||||||
this["E8"] = null;
|
this["E8"] = null;
|
||||||
this["F8"] = null;
|
this["F8"] = null;
|
||||||
this["G8"] = null;
|
this["G8"] = null;
|
||||||
this["H8"] = new Bishop(WhichPlayer.Player2);
|
this["H8"] = new Bishop(WhichPlayer.Player2);
|
||||||
this["I8"] = null;
|
this["I8"] = null;
|
||||||
|
|
||||||
this["A9"] = new Lance(WhichPlayer.Player2);
|
this["A9"] = new Lance(WhichPlayer.Player2);
|
||||||
this["B9"] = new Knight(WhichPlayer.Player2);
|
this["B9"] = new Knight(WhichPlayer.Player2);
|
||||||
this["C9"] = new SilverGeneral(WhichPlayer.Player2);
|
this["C9"] = new SilverGeneral(WhichPlayer.Player2);
|
||||||
this["D9"] = new GoldGeneral(WhichPlayer.Player2);
|
this["D9"] = new GoldGeneral(WhichPlayer.Player2);
|
||||||
this["E9"] = new King(WhichPlayer.Player2);
|
this["E9"] = new King(WhichPlayer.Player2);
|
||||||
this["F9"] = new GoldGeneral(WhichPlayer.Player2);
|
this["F9"] = new GoldGeneral(WhichPlayer.Player2);
|
||||||
this["G9"] = new SilverGeneral(WhichPlayer.Player2);
|
this["G9"] = new SilverGeneral(WhichPlayer.Player2);
|
||||||
this["H9"] = new Knight(WhichPlayer.Player2);
|
this["H9"] = new Knight(WhichPlayer.Player2);
|
||||||
this["I9"] = new Lance(WhichPlayer.Player2);
|
this["I9"] = new Lance(WhichPlayer.Player2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using Shogi.Domain.Pieces;
|
using Shogi.Domain.ValueObjects;
|
||||||
|
|
||||||
namespace Shogi.Domain
|
namespace Shogi.Domain
|
||||||
{
|
{
|
||||||
internal static class DomainExtensions
|
internal static class DomainExtensions
|
||||||
{
|
{
|
||||||
public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King;
|
public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
using Shogi.Domain.Pieces;
|
using System.Diagnostics;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pathing
|
namespace Shogi.Domain.Pathing
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
internal class Bishop : Piece
|
|
||||||
{
|
|
||||||
private static readonly ReadOnlyCollection<Path> BishopPaths = new(new List<Path>(4)
|
|
||||||
{
|
|
||||||
new Path(Direction.UpLeft, Distance.MultiStep),
|
|
||||||
new Path(Direction.UpRight, Distance.MultiStep),
|
|
||||||
new Path(Direction.DownLeft, Distance.MultiStep),
|
|
||||||
new Path(Direction.DownRight, Distance.MultiStep)
|
|
||||||
});
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> PromotedBishopPaths = new(new List<Path>(8)
|
|
||||||
{
|
|
||||||
new Path(Direction.Up),
|
|
||||||
new Path(Direction.Left),
|
|
||||||
new Path(Direction.Right),
|
|
||||||
new Path(Direction.Down),
|
|
||||||
new Path(Direction.UpLeft, Distance.MultiStep),
|
|
||||||
new Path(Direction.UpRight, Distance.MultiStep),
|
|
||||||
new Path(Direction.DownLeft, Distance.MultiStep),
|
|
||||||
new Path(Direction.DownRight, Distance.MultiStep)
|
|
||||||
});
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
|
||||||
BishopPaths
|
|
||||||
.Select(p => p.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2PromotedPaths =
|
|
||||||
PromotedBishopPaths
|
|
||||||
.Select(p => p.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public Bishop(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.Bishop, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IEnumerable<Path> MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
internal class GoldGeneral : Piece
|
|
||||||
{
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(6)
|
|
||||||
{
|
|
||||||
new Path(Direction.Up),
|
|
||||||
new Path(Direction.UpLeft),
|
|
||||||
new Path(Direction.UpRight),
|
|
||||||
new Path(Direction.Left),
|
|
||||||
new Path(Direction.Right),
|
|
||||||
new Path(Direction.Down)
|
|
||||||
});
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
|
||||||
Player1Paths
|
|
||||||
.Select(p => p.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public GoldGeneral(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.GoldGeneral, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IEnumerable<Path> MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
internal class King : Piece
|
|
||||||
{
|
|
||||||
internal static readonly ReadOnlyCollection<Path> KingPaths = new(new List<Path>(8)
|
|
||||||
{
|
|
||||||
new Path(Direction.Up),
|
|
||||||
new Path(Direction.Left),
|
|
||||||
new Path(Direction.Right),
|
|
||||||
new Path(Direction.Down),
|
|
||||||
new Path(Direction.UpLeft),
|
|
||||||
new Path(Direction.UpRight),
|
|
||||||
new Path(Direction.DownLeft),
|
|
||||||
new Path(Direction.DownRight)
|
|
||||||
});
|
|
||||||
|
|
||||||
public King(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.King, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IEnumerable<Path> MoveSet => KingPaths;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
internal class Knight : Piece
|
|
||||||
{
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(2)
|
|
||||||
{
|
|
||||||
new Path(Direction.KnightLeft),
|
|
||||||
new Path(Direction.KnightRight)
|
|
||||||
});
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
|
||||||
Player1Paths
|
|
||||||
.Select(p => p.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public Knight(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.Knight, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
|
||||||
{
|
|
||||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
|
||||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
|
||||||
_ => throw new NotImplementedException(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
internal class Lance : Piece
|
|
||||||
{
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
|
||||||
{
|
|
||||||
new Path(Direction.Up, Distance.MultiStep),
|
|
||||||
});
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
|
||||||
Player1Paths
|
|
||||||
.Select(p => p.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public Lance(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.Lance, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
|
||||||
{
|
|
||||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
|
||||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
|
||||||
_ => throw new NotImplementedException(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
internal class Pawn : Piece
|
|
||||||
{
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
|
||||||
{
|
|
||||||
new Path(Direction.Up)
|
|
||||||
});
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
|
||||||
Player1Paths
|
|
||||||
.Select(p => p.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public Pawn(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.Pawn, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
|
||||||
{
|
|
||||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
|
||||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
|
||||||
_ => throw new NotImplementedException(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
[DebuggerDisplay("{WhichPiece} {Owner}")]
|
|
||||||
public abstract class Piece
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a clone of an existing piece.
|
|
||||||
/// </summary>
|
|
||||||
public static Piece CreateCopy(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
|
|
||||||
public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
|
||||||
{
|
|
||||||
return piece switch
|
|
||||||
{
|
|
||||||
WhichPiece.King => new King(owner, isPromoted),
|
|
||||||
WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted),
|
|
||||||
WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted),
|
|
||||||
WhichPiece.Bishop => new Bishop(owner, isPromoted),
|
|
||||||
WhichPiece.Rook => new Rook(owner, isPromoted),
|
|
||||||
WhichPiece.Knight => new Knight(owner, isPromoted),
|
|
||||||
WhichPiece.Lance => new Lance(owner, isPromoted),
|
|
||||||
WhichPiece.Pawn => new Pawn(owner, isPromoted),
|
|
||||||
_ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public abstract IEnumerable<Path> MoveSet { get; }
|
|
||||||
public WhichPiece WhichPiece { get; }
|
|
||||||
public WhichPlayer Owner { get; private set; }
|
|
||||||
public bool IsPromoted { get; private set; }
|
|
||||||
public bool IsUpsideDown => Owner == WhichPlayer.Player2;
|
|
||||||
|
|
||||||
protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
|
||||||
{
|
|
||||||
WhichPiece = piece;
|
|
||||||
Owner = owner;
|
|
||||||
IsPromoted = isPromoted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanPromote => !IsPromoted
|
|
||||||
&& WhichPiece != WhichPiece.King
|
|
||||||
&& WhichPiece != WhichPiece.GoldGeneral;
|
|
||||||
|
|
||||||
public void Promote() => IsPromoted = CanPromote;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prep the piece for capture by changing ownership and demoting.
|
|
||||||
/// </summary>
|
|
||||||
public void Capture(WhichPlayer newOwner)
|
|
||||||
{
|
|
||||||
Owner = newOwner;
|
|
||||||
IsPromoted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Respecting the move-set of the Piece, collect all positions from start to end.
|
|
||||||
/// Useful if you need to iterate a move-set.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="start"></param>
|
|
||||||
/// <param name="end"></param>
|
|
||||||
/// <returns>An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions.</returns>
|
|
||||||
public IEnumerable<Vector2> GetPathFromStartToEnd(Vector2 start, Vector2 end)
|
|
||||||
{
|
|
||||||
var steps = new List<Vector2>(10);
|
|
||||||
|
|
||||||
var path = this.MoveSet.GetNearestPath(start, end);
|
|
||||||
var position = start;
|
|
||||||
while (Vector2.Distance(start, position) < Vector2.Distance(start, end))
|
|
||||||
{
|
|
||||||
position += path.Direction;
|
|
||||||
steps.Add(position);
|
|
||||||
|
|
||||||
if (path.Distance == Distance.OneStep) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position == end)
|
|
||||||
{
|
|
||||||
return steps;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.Empty<Vector2>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all positions this piece could move to from the currentPosition, respecting the move-set of this piece.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="currentPosition"></param>
|
|
||||||
/// <returns>A list of positions the piece could move to.</returns>
|
|
||||||
public IEnumerable<Vector2> GetPossiblePositions(Vector2 currentPosition)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
public sealed class Rook : Piece
|
|
||||||
{
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(4)
|
|
||||||
{
|
|
||||||
new Path(Direction.Up, Distance.MultiStep),
|
|
||||||
new Path(Direction.Left, Distance.MultiStep),
|
|
||||||
new Path(Direction.Right, Distance.MultiStep),
|
|
||||||
new Path(Direction.Down, Distance.MultiStep)
|
|
||||||
});
|
|
||||||
|
|
||||||
private static readonly ReadOnlyCollection<Path> PromotedPlayer1Paths = new(new List<Path>(8)
|
|
||||||
{
|
|
||||||
new Path(Direction.Up, Distance.MultiStep),
|
|
||||||
new Path(Direction.Left, Distance.MultiStep),
|
|
||||||
new Path(Direction.Right, Distance.MultiStep),
|
|
||||||
new Path(Direction.Down, Distance.MultiStep),
|
|
||||||
new Path(Direction.UpLeft),
|
|
||||||
new Path(Direction.UpRight),
|
|
||||||
new Path(Direction.DownLeft),
|
|
||||||
new Path(Direction.DownRight)
|
|
||||||
});
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
|
||||||
Player1Paths
|
|
||||||
.Select(m => m.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2PromotedPaths =
|
|
||||||
PromotedPlayer1Paths
|
|
||||||
.Select(m => m.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public Rook(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.Rook, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
|
||||||
{
|
|
||||||
WhichPlayer.Player1 => IsPromoted ? PromotedPlayer1Paths : Player1Paths,
|
|
||||||
WhichPlayer.Player2 => IsPromoted ? Player2PromotedPaths : Player2Paths,
|
|
||||||
_ => throw new NotImplementedException(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using Shogi.Domain.Pathing;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Shogi.Domain.Pieces
|
|
||||||
{
|
|
||||||
internal class SilverGeneral : Piece
|
|
||||||
{
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(4)
|
|
||||||
{
|
|
||||||
new Path(Direction.Up),
|
|
||||||
new Path(Direction.UpLeft),
|
|
||||||
new Path(Direction.UpRight),
|
|
||||||
new Path(Direction.DownLeft),
|
|
||||||
new Path(Direction.DownRight)
|
|
||||||
});
|
|
||||||
|
|
||||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
|
||||||
Player1Paths
|
|
||||||
.Select(p => p.Invert())
|
|
||||||
.ToList()
|
|
||||||
.AsReadOnly();
|
|
||||||
|
|
||||||
public SilverGeneral(WhichPlayer owner, bool isPromoted = false)
|
|
||||||
: base(WhichPiece.SilverGeneral, owner, isPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
|
||||||
{
|
|
||||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
|
||||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
|
||||||
_ => throw new NotImplementedException(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
using Shogi.Domain.Pieces;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Shogi.Domain
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Facilitates Shogi board state transitions, cognisant of Shogi rules.
|
|
||||||
/// The board is always from Player1's perspective.
|
|
||||||
/// [0,0] is the lower-left position, [8,8] is the higher-right position
|
|
||||||
/// </summary>
|
|
||||||
public sealed class Session
|
|
||||||
{
|
|
||||||
private readonly Tuple<string, string> Players;
|
|
||||||
private readonly BoardState boardState;
|
|
||||||
private readonly StandardRules rules;
|
|
||||||
private readonly SessionMetadata metadata;
|
|
||||||
|
|
||||||
public Session() : this(new BoardState())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Session(BoardState state) : this(state, new SessionMetadata(string.Empty, false, string.Empty, string.Empty))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Session(BoardState state, SessionMetadata metadata)
|
|
||||||
{
|
|
||||||
rules = new StandardRules(state);
|
|
||||||
boardState = state;
|
|
||||||
this.metadata = metadata;
|
|
||||||
Players = new(string.Empty, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name => metadata.Name;
|
|
||||||
public string Player1Name => metadata.Player1;
|
|
||||||
public string? Player2Name => metadata.Player2;
|
|
||||||
public IDictionary<string, Piece?> BoardState => boardState.State;
|
|
||||||
public IList<Piece> Player1Hand => boardState.Player1Hand;
|
|
||||||
public IList<Piece> Player2Hand => boardState.Player2Hand;
|
|
||||||
public WhichPlayer? InCheck => boardState.InCheck;
|
|
||||||
public bool IsCheckMate => boardState.IsCheckmate;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The strategy involves simulating a move on a throw-away board state that can be used to
|
|
||||||
/// validate legal vs illegal moves without having to worry about reverting board state.
|
|
||||||
/// </remarks>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public void Move(string from, string to, bool isPromotion)
|
|
||||||
{
|
|
||||||
var simulationState = new BoardState(boardState);
|
|
||||||
var simulation = new StandardRules(simulationState);
|
|
||||||
var moveResult = simulation.Move(from, to, isPromotion);
|
|
||||||
if (!moveResult.Success)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(moveResult.Reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If already in check, assert the move that resulted in check no longer results in check.
|
|
||||||
if (boardState.InCheck == boardState.WhoseTurn
|
|
||||||
&& simulation.IsOpposingKingThreatenedByPosition(boardState.PreviousMoveTo))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Unable to move because you are still in check.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
|
||||||
if (simulation.IsPlayerInCheckAfterMove())
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Illegal move. This move places you in check.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = rules.Move(from, to, isPromotion);
|
|
||||||
if (rules.IsOpponentInCheckAfterMove())
|
|
||||||
{
|
|
||||||
boardState.InCheck = otherPlayer;
|
|
||||||
if (rules.IsOpponentInCheckMate())
|
|
||||||
{
|
|
||||||
boardState.IsCheckmate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
boardState.InCheck = null;
|
|
||||||
}
|
|
||||||
boardState.WhoseTurn = otherPlayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Move(WhichPiece pieceInHand, string to)
|
|
||||||
{
|
|
||||||
var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
|
|
||||||
if (index == -1)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"{pieceInHand} does not exist in the hand.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (boardState[to] != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var toVector = Notation.FromBoardNotation(to);
|
|
||||||
switch (pieceInHand)
|
|
||||||
{
|
|
||||||
case WhichPiece.Knight:
|
|
||||||
{
|
|
||||||
// Knight cannot be placed onto the farthest two ranks from the hand.
|
|
||||||
if ((boardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6)
|
|
||||||
|| (boardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WhichPiece.Lance:
|
|
||||||
case WhichPiece.Pawn:
|
|
||||||
{
|
|
||||||
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
|
|
||||||
if ((boardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8)
|
|
||||||
|| (boardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempBoard = new BoardState(boardState);
|
|
||||||
var simulation = new StandardRules(tempBoard);
|
|
||||||
var moveResult = simulation.Move(pieceInHand, to);
|
|
||||||
if (!moveResult.Success)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(moveResult.Reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
|
||||||
if (boardState.InCheck == boardState.WhoseTurn)
|
|
||||||
{
|
|
||||||
//if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn))
|
|
||||||
//{
|
|
||||||
// throw new InvalidOperationException("Illegal move. You're still in check!");
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition;
|
|
||||||
//if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer))
|
|
||||||
//{
|
|
||||||
|
|
||||||
//}
|
|
||||||
|
|
||||||
//rules.Move(from, to, isPromotion);
|
|
||||||
//if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer))
|
|
||||||
//{
|
|
||||||
// board.InCheck = otherPlayer;
|
|
||||||
// board.IsCheckmate = rules.EvaluateCheckmate();
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
//{
|
|
||||||
// board.InCheck = null;
|
|
||||||
//}
|
|
||||||
boardState.WhoseTurn = otherPlayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prints a ASCII representation of the board for debugging board state.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public string ToStringStateAsAscii()
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.Append(" ");
|
|
||||||
builder.Append("Player 2(.)");
|
|
||||||
builder.AppendLine();
|
|
||||||
for (var rank = 8; rank >= 0; rank--)
|
|
||||||
{
|
|
||||||
// Horizontal line
|
|
||||||
builder.Append(" - ");
|
|
||||||
for (var file = 0; file < 8; file++) builder.Append("- - ");
|
|
||||||
builder.Append("- -");
|
|
||||||
|
|
||||||
// Print Rank ruler.
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.Append($"{rank + 1} ");
|
|
||||||
|
|
||||||
// Print pieces.
|
|
||||||
builder.Append(" |");
|
|
||||||
for (var x = 0; x < 9; x++)
|
|
||||||
{
|
|
||||||
var piece = boardState[x, rank];
|
|
||||||
if (piece == null)
|
|
||||||
{
|
|
||||||
builder.Append(" ");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
builder.AppendFormat("{0}", ToAscii(piece));
|
|
||||||
}
|
|
||||||
builder.Append('|');
|
|
||||||
}
|
|
||||||
builder.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Horizontal line
|
|
||||||
builder.Append(" - ");
|
|
||||||
for (var x = 0; x < 8; x++) builder.Append("- - ");
|
|
||||||
builder.Append("- -");
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.Append(" ");
|
|
||||||
builder.Append("Player 1");
|
|
||||||
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.AppendLine();
|
|
||||||
// Print File ruler.
|
|
||||||
builder.Append(" ");
|
|
||||||
builder.Append(" A B C D E F G H I ");
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="piece"></param>
|
|
||||||
/// <returns>
|
|
||||||
/// A string with three characters.
|
|
||||||
/// The first character indicates promotion status.
|
|
||||||
/// The second character indicates piece.
|
|
||||||
/// The third character indicates ownership.
|
|
||||||
/// </returns>
|
|
||||||
public static string ToAscii(Piece piece)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
if (piece.IsPromoted) builder.Append('^');
|
|
||||||
else builder.Append(' ');
|
|
||||||
|
|
||||||
var name = piece.WhichPiece switch
|
|
||||||
{
|
|
||||||
WhichPiece.King => "K",
|
|
||||||
WhichPiece.GoldGeneral => "G",
|
|
||||||
WhichPiece.SilverGeneral => "S",
|
|
||||||
WhichPiece.Bishop => "B",
|
|
||||||
WhichPiece.Rook => "R",
|
|
||||||
WhichPiece.Knight => "k",
|
|
||||||
WhichPiece.Lance => "L",
|
|
||||||
WhichPiece.Pawn => "P",
|
|
||||||
_ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."),
|
|
||||||
};
|
|
||||||
builder.Append(name);
|
|
||||||
|
|
||||||
if (piece.Owner == WhichPlayer.Player2) builder.Append('.');
|
|
||||||
else builder.Append(' ');
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace Shogi.Domain
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A representation of a Session without the board and game-rules.
|
|
||||||
/// </summary>
|
|
||||||
public class SessionMetadata
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public string Player1 { get; }
|
|
||||||
public string? Player2 { get; private set; }
|
|
||||||
public bool IsPrivate { get; }
|
|
||||||
|
|
||||||
public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
IsPrivate = isPrivate;
|
|
||||||
Player1 = player1;
|
|
||||||
Player2 = player2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPlayer2(string user)
|
|
||||||
{
|
|
||||||
Player2 = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsSeated(string playerName) => playerName == Player1 || playerName == Player2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="System"/>
|
<Using Include="System" />
|
||||||
<Using Include="System.Collections.Generic"/>
|
<Using Include="System.Collections.Generic" />
|
||||||
<Using Include="System.Linq"/>
|
<Using Include="System.Linq" />
|
||||||
<Using Include="System.Numerics"/>
|
<Using Include="System.Numerics" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Entities\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using Shogi.Domain.Pathing;
|
using Shogi.Domain.Pathing;
|
||||||
using Shogi.Domain.Pieces;
|
using Shogi.Domain.ValueObjects;
|
||||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.Pieces.Piece>;
|
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
||||||
|
|
||||||
namespace Shogi.Domain
|
namespace Shogi.Domain
|
||||||
{
|
{
|
||||||
internal class StandardRules
|
internal class StandardRules
|
||||||
{
|
{
|
||||||
private readonly BoardState boardState;
|
private readonly BoardState boardState;
|
||||||
|
|
||||||
|
|||||||
47
Shogi.Domain/ValueObjects/Bishop.cs
Normal file
47
Shogi.Domain/ValueObjects/Bishop.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
internal record class Bishop : Piece
|
||||||
|
{
|
||||||
|
private static readonly ReadOnlyCollection<Path> BishopPaths = new(new List<Path>(4)
|
||||||
|
{
|
||||||
|
new Path(Direction.UpLeft, Distance.MultiStep),
|
||||||
|
new Path(Direction.UpRight, Distance.MultiStep),
|
||||||
|
new Path(Direction.DownLeft, Distance.MultiStep),
|
||||||
|
new Path(Direction.DownRight, Distance.MultiStep)
|
||||||
|
});
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> PromotedBishopPaths = new(new List<Path>(8)
|
||||||
|
{
|
||||||
|
new Path(Direction.Up),
|
||||||
|
new Path(Direction.Left),
|
||||||
|
new Path(Direction.Right),
|
||||||
|
new Path(Direction.Down),
|
||||||
|
new Path(Direction.UpLeft, Distance.MultiStep),
|
||||||
|
new Path(Direction.UpRight, Distance.MultiStep),
|
||||||
|
new Path(Direction.DownLeft, Distance.MultiStep),
|
||||||
|
new Path(Direction.DownRight, Distance.MultiStep)
|
||||||
|
});
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||||
|
BishopPaths
|
||||||
|
.Select(p => p.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2PromotedPaths =
|
||||||
|
PromotedBishopPaths
|
||||||
|
.Select(p => p.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public Bishop(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.Bishop, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<Path> MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Shogi.Domain/ValueObjects/GoldGeneral.cs
Normal file
31
Shogi.Domain/ValueObjects/GoldGeneral.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
internal record class GoldGeneral : Piece
|
||||||
|
{
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(6)
|
||||||
|
{
|
||||||
|
new Path(Direction.Up),
|
||||||
|
new Path(Direction.UpLeft),
|
||||||
|
new Path(Direction.UpRight),
|
||||||
|
new Path(Direction.Left),
|
||||||
|
new Path(Direction.Right),
|
||||||
|
new Path(Direction.Down)
|
||||||
|
});
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||||
|
Player1Paths
|
||||||
|
.Select(p => p.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public GoldGeneral(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.GoldGeneral, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<Path> MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Shogi.Domain/ValueObjects/King.cs
Normal file
27
Shogi.Domain/ValueObjects/King.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
internal record class King : Piece
|
||||||
|
{
|
||||||
|
internal static readonly ReadOnlyCollection<Path> KingPaths = new(new List<Path>(8)
|
||||||
|
{
|
||||||
|
new Path(Direction.Up),
|
||||||
|
new Path(Direction.Left),
|
||||||
|
new Path(Direction.Right),
|
||||||
|
new Path(Direction.Down),
|
||||||
|
new Path(Direction.UpLeft),
|
||||||
|
new Path(Direction.UpRight),
|
||||||
|
new Path(Direction.DownLeft),
|
||||||
|
new Path(Direction.DownRight)
|
||||||
|
});
|
||||||
|
|
||||||
|
public King(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.King, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<Path> MoveSet => KingPaths;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Shogi.Domain/ValueObjects/Knight.cs
Normal file
32
Shogi.Domain/ValueObjects/Knight.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
internal record class Knight : Piece
|
||||||
|
{
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(2)
|
||||||
|
{
|
||||||
|
new Path(Direction.KnightLeft),
|
||||||
|
new Path(Direction.KnightRight)
|
||||||
|
});
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||||
|
Player1Paths
|
||||||
|
.Select(p => p.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public Knight(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.Knight, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||||
|
{
|
||||||
|
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||||
|
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Shogi.Domain/ValueObjects/Lance.cs
Normal file
31
Shogi.Domain/ValueObjects/Lance.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
internal record class Lance : Piece
|
||||||
|
{
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
||||||
|
{
|
||||||
|
new Path(Direction.Up, Distance.MultiStep),
|
||||||
|
});
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||||
|
Player1Paths
|
||||||
|
.Select(p => p.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public Lance(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.Lance, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||||
|
{
|
||||||
|
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||||
|
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Shogi.Domain/ValueObjects/Pawn.cs
Normal file
31
Shogi.Domain/ValueObjects/Pawn.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
internal record class Pawn : Piece
|
||||||
|
{
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
||||||
|
{
|
||||||
|
new Path(Direction.Up)
|
||||||
|
});
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||||
|
Player1Paths
|
||||||
|
.Select(p => p.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public Pawn(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.Pawn, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||||
|
{
|
||||||
|
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||||
|
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
95
Shogi.Domain/ValueObjects/Piece.cs
Normal file
95
Shogi.Domain/ValueObjects/Piece.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{WhichPiece} {Owner}")]
|
||||||
|
public abstract record class Piece
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a clone of an existing piece.
|
||||||
|
/// </summary>
|
||||||
|
public static Piece CreateCopy(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
|
||||||
|
public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
||||||
|
{
|
||||||
|
return piece switch
|
||||||
|
{
|
||||||
|
WhichPiece.King => new King(owner, isPromoted),
|
||||||
|
WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted),
|
||||||
|
WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted),
|
||||||
|
WhichPiece.Bishop => new Bishop(owner, isPromoted),
|
||||||
|
WhichPiece.Rook => new Rook(owner, isPromoted),
|
||||||
|
WhichPiece.Knight => new Knight(owner, isPromoted),
|
||||||
|
WhichPiece.Lance => new Lance(owner, isPromoted),
|
||||||
|
WhichPiece.Pawn => new Pawn(owner, isPromoted),
|
||||||
|
_ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public abstract IEnumerable<Path> MoveSet { get; }
|
||||||
|
public WhichPiece WhichPiece { get; }
|
||||||
|
public WhichPlayer Owner { get; private set; }
|
||||||
|
public bool IsPromoted { get; private set; }
|
||||||
|
public bool IsUpsideDown => Owner == WhichPlayer.Player2;
|
||||||
|
|
||||||
|
protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
||||||
|
{
|
||||||
|
WhichPiece = piece;
|
||||||
|
Owner = owner;
|
||||||
|
IsPromoted = isPromoted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanPromote => !IsPromoted
|
||||||
|
&& WhichPiece != WhichPiece.King
|
||||||
|
&& WhichPiece != WhichPiece.GoldGeneral;
|
||||||
|
|
||||||
|
public void Promote() => IsPromoted = CanPromote;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prep the piece for capture by changing ownership and demoting.
|
||||||
|
/// </summary>
|
||||||
|
public void Capture(WhichPlayer newOwner)
|
||||||
|
{
|
||||||
|
Owner = newOwner;
|
||||||
|
IsPromoted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Respecting the move-set of the Piece, collect all positions from start to end.
|
||||||
|
/// Useful if you need to iterate a move-set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start"></param>
|
||||||
|
/// <param name="end"></param>
|
||||||
|
/// <returns>An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions.</returns>
|
||||||
|
public IEnumerable<Vector2> GetPathFromStartToEnd(Vector2 start, Vector2 end)
|
||||||
|
{
|
||||||
|
var steps = new List<Vector2>(10);
|
||||||
|
|
||||||
|
var path = MoveSet.GetNearestPath(start, end);
|
||||||
|
var position = start;
|
||||||
|
while (Vector2.Distance(start, position) < Vector2.Distance(start, end))
|
||||||
|
{
|
||||||
|
position += path.Direction;
|
||||||
|
steps.Add(position);
|
||||||
|
|
||||||
|
if (path.Distance == Distance.OneStep) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position == end)
|
||||||
|
{
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.Empty<Vector2>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all positions this piece could move to from the currentPosition, respecting the move-set of this piece.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentPosition"></param>
|
||||||
|
/// <returns>A list of positions the piece could move to.</returns>
|
||||||
|
public IEnumerable<Vector2> GetPossiblePositions(Vector2 currentPosition)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Shogi.Domain/ValueObjects/Rook.cs
Normal file
51
Shogi.Domain/ValueObjects/Rook.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects;
|
||||||
|
|
||||||
|
public record class Rook : Piece
|
||||||
|
{
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(4)
|
||||||
|
{
|
||||||
|
new Path(Direction.Up, Distance.MultiStep),
|
||||||
|
new Path(Direction.Left, Distance.MultiStep),
|
||||||
|
new Path(Direction.Right, Distance.MultiStep),
|
||||||
|
new Path(Direction.Down, Distance.MultiStep)
|
||||||
|
});
|
||||||
|
|
||||||
|
private static readonly ReadOnlyCollection<Path> PromotedPlayer1Paths = new(new List<Path>(8)
|
||||||
|
{
|
||||||
|
new Path(Direction.Up, Distance.MultiStep),
|
||||||
|
new Path(Direction.Left, Distance.MultiStep),
|
||||||
|
new Path(Direction.Right, Distance.MultiStep),
|
||||||
|
new Path(Direction.Down, Distance.MultiStep),
|
||||||
|
new Path(Direction.UpLeft),
|
||||||
|
new Path(Direction.UpRight),
|
||||||
|
new Path(Direction.DownLeft),
|
||||||
|
new Path(Direction.DownRight)
|
||||||
|
});
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||||
|
Player1Paths
|
||||||
|
.Select(m => m.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2PromotedPaths =
|
||||||
|
PromotedPlayer1Paths
|
||||||
|
.Select(m => m.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public Rook(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.Rook, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||||
|
{
|
||||||
|
WhichPlayer.Player1 => IsPromoted ? PromotedPlayer1Paths : Player1Paths,
|
||||||
|
WhichPlayer.Player2 => IsPromoted ? Player2PromotedPaths : Player2Paths,
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
35
Shogi.Domain/ValueObjects/SilverGeneral.cs
Normal file
35
Shogi.Domain/ValueObjects/SilverGeneral.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Shogi.Domain.Pathing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
internal record class SilverGeneral : Piece
|
||||||
|
{
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(4)
|
||||||
|
{
|
||||||
|
new Path(Direction.Up),
|
||||||
|
new Path(Direction.UpLeft),
|
||||||
|
new Path(Direction.UpRight),
|
||||||
|
new Path(Direction.DownLeft),
|
||||||
|
new Path(Direction.DownRight)
|
||||||
|
});
|
||||||
|
|
||||||
|
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||||
|
Player1Paths
|
||||||
|
.Select(p => p.Invert())
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public SilverGeneral(WhichPlayer owner, bool isPromoted = false)
|
||||||
|
: base(WhichPiece.SilverGeneral, owner, isPromoted)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||||
|
{
|
||||||
|
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||||
|
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
217
Shogi.Sockets/Controllers/SessionController.cs
Normal file
217
Shogi.Sockets/Controllers/SessionController.cs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
using Shogi.Api.Managers;
|
||||||
|
using Shogi.Api.Repositories;
|
||||||
|
using Shogi.Contracts.Api;
|
||||||
|
using Shogi.Contracts.Socket;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Data.SqlClient;
|
||||||
|
using Shogi.Contracts.Types;
|
||||||
|
using Shogi.Api.Extensions;
|
||||||
|
|
||||||
|
namespace Shogi.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class SessionController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ISocketConnectionManager communicationManager;
|
||||||
|
private readonly IModelMapper mapper;
|
||||||
|
private readonly ISessionRepository sessionRepository;
|
||||||
|
private readonly IQueryRespository queryRespository;
|
||||||
|
|
||||||
|
public SessionController(
|
||||||
|
ISocketConnectionManager communicationManager,
|
||||||
|
IModelMapper mapper,
|
||||||
|
ISessionRepository sessionRepository,
|
||||||
|
IQueryRespository queryRespository)
|
||||||
|
{
|
||||||
|
this.communicationManager = communicationManager;
|
||||||
|
this.mapper = mapper;
|
||||||
|
this.sessionRepository = sessionRepository;
|
||||||
|
this.queryRespository = queryRespository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateSession([FromBody] CreateSessionCommand request)
|
||||||
|
{
|
||||||
|
var userId = User.GetShogiUserId();
|
||||||
|
if (string.IsNullOrWhiteSpace(userId)) return this.Unauthorized();
|
||||||
|
var session = new Domain.Session(request.Name, Domain.BoardState.StandardStarting, userId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await sessionRepository.CreateSession(session);
|
||||||
|
}
|
||||||
|
catch (SqlException)
|
||||||
|
{
|
||||||
|
return this.Conflict();
|
||||||
|
}
|
||||||
|
|
||||||
|
await communicationManager.BroadcastToAll(new SessionCreatedSocketMessage());
|
||||||
|
return CreatedAtAction(nameof(CreateSession), new { sessionName = request.Name }, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
//[HttpPost("{sessionName}/Move")]
|
||||||
|
//public async Task<IActionResult> MovePiece([FromRoute] string sessionName, [FromBody] MovePieceCommand request)
|
||||||
|
//{
|
||||||
|
|
||||||
|
// var user = await gameboardManager.ReadUser(User);
|
||||||
|
// var session = await gameboardRepository.ReadSession(sessionName);
|
||||||
|
// if (session == null)
|
||||||
|
// {
|
||||||
|
// return NotFound();
|
||||||
|
// }
|
||||||
|
// if (user == null || (session.Player1 != user.Id && session.Player2 != user.Id))
|
||||||
|
// {
|
||||||
|
// return Forbid("User is not seated at this game.");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var move = request.Move;
|
||||||
|
// if (move.PieceFromCaptured.HasValue)
|
||||||
|
// session.Move(mapper.Map(move.PieceFromCaptured.Value), move.To);
|
||||||
|
// else if (!string.IsNullOrWhiteSpace(move.From))
|
||||||
|
// session.Move(move.From, move.To, move.IsPromotion);
|
||||||
|
|
||||||
|
// await gameboardRepository.CreateBoardState(session);
|
||||||
|
// await communicationManager.BroadcastToPlayers(
|
||||||
|
// new MoveResponse
|
||||||
|
// {
|
||||||
|
// SessionName = session.Name,
|
||||||
|
// PlayerName = user.Id
|
||||||
|
// },
|
||||||
|
// session.Player1,
|
||||||
|
// session.Player2);
|
||||||
|
|
||||||
|
// return Ok();
|
||||||
|
// }
|
||||||
|
// catch (InvalidOperationException ex)
|
||||||
|
// {
|
||||||
|
// return Conflict(ex.Message);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
// TODO: Use JWT tokens for guests so they can authenticate and use API routes, too.
|
||||||
|
//[Route("")]
|
||||||
|
//public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
||||||
|
//{
|
||||||
|
// var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2);
|
||||||
|
// var success = await repository.CreateSession(model);
|
||||||
|
// if (success)
|
||||||
|
// {
|
||||||
|
// var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.SocketAction.CreateGame)
|
||||||
|
// {
|
||||||
|
// Game = model.ToServiceModel(),
|
||||||
|
// PlayerName =
|
||||||
|
// }
|
||||||
|
// var task = request.IsPrivate
|
||||||
|
// ? communicationManager.BroadcastToPlayers(response, userName)
|
||||||
|
// : communicationManager.BroadcastToAll(response);
|
||||||
|
// return new CreatedResult("", null);
|
||||||
|
// }
|
||||||
|
// return new ConflictResult();
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//[HttpGet("{sessionName}")]
|
||||||
|
//[AllowAnonymous]
|
||||||
|
//public async Task<IActionResult> GetSession([FromRoute] string sessionName)
|
||||||
|
//{
|
||||||
|
// var user = await ReadUserOrThrow();
|
||||||
|
// var session = await gameboardRepository.ReadSession(sessionName);
|
||||||
|
// if (session == null)
|
||||||
|
// {
|
||||||
|
// return NotFound();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var playerPerspective = session.Player2 == user.Id
|
||||||
|
// ? WhichPlayer.Player2
|
||||||
|
// : WhichPlayer.Player1;
|
||||||
|
|
||||||
|
// var response = new ReadSessionResponse
|
||||||
|
// {
|
||||||
|
// Session = new Session
|
||||||
|
// {
|
||||||
|
// BoardState = new BoardState
|
||||||
|
// {
|
||||||
|
// Board = mapper.Map(session.BoardState.State),
|
||||||
|
// Player1Hand = session.BoardState.Player1Hand.Select(mapper.Map).ToList(),
|
||||||
|
// Player2Hand = session.BoardState.Player2Hand.Select(mapper.Map).ToList(),
|
||||||
|
// PlayerInCheck = mapper.Map(session.BoardState.InCheck)
|
||||||
|
// },
|
||||||
|
// SessionName = session.Name,
|
||||||
|
// Player1 = session.Player1,
|
||||||
|
// Player2 = session.Player2
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// return Ok(response);
|
||||||
|
//}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<ActionResult<ReadAllSessionsResponse>> GetSessions()
|
||||||
|
{
|
||||||
|
var sessions = await this.queryRespository.ReadAllSessionsMetadata();
|
||||||
|
|
||||||
|
return Ok(new ReadAllSessionsResponse
|
||||||
|
{
|
||||||
|
PlayerHasJoinedSessions = Array.Empty<SessionMetadata>(),
|
||||||
|
AllOtherSessions = sessions.ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//[HttpPut("{sessionName}")]
|
||||||
|
//public async Task<IActionResult> PutJoinSession([FromRoute] string sessionName)
|
||||||
|
//{
|
||||||
|
// var user = await ReadUserOrThrow();
|
||||||
|
// var session = await gameboardRepository.ReadSessionMetaData(sessionName);
|
||||||
|
// if (session == null)
|
||||||
|
// {
|
||||||
|
// return NotFound();
|
||||||
|
// }
|
||||||
|
// if (session.Player2 != null)
|
||||||
|
// {
|
||||||
|
// return this.Conflict("This session already has two seated players and is full.");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// session.SetPlayer2(user.Id);
|
||||||
|
// await gameboardRepository.UpdateSession(session);
|
||||||
|
|
||||||
|
// var opponentName = user.Id == session.Player1
|
||||||
|
// ? session.Player2!
|
||||||
|
// : session.Player1;
|
||||||
|
// await communicationManager.BroadcastToPlayers(new JoinSessionResponse
|
||||||
|
// {
|
||||||
|
// SessionName = session.Name,
|
||||||
|
// PlayerName = user.Id
|
||||||
|
// }, opponentName);
|
||||||
|
// return Ok();
|
||||||
|
//}
|
||||||
|
|
||||||
|
//[Authorize(Roles = "Admin")]
|
||||||
|
//[HttpDelete("{sessionName}")]
|
||||||
|
//public async Task<IActionResult> DeleteSession([FromRoute] string sessionName)
|
||||||
|
//{
|
||||||
|
// var user = await ReadUserOrThrow();
|
||||||
|
// if (user.IsAdmin)
|
||||||
|
// {
|
||||||
|
// return Ok();
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// return Unauthorized();
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//private async Task<Models.User> ReadUserOrThrow()
|
||||||
|
//{
|
||||||
|
// var user = await gameboardManager.ReadUser(User);
|
||||||
|
// if (user == null)
|
||||||
|
// {
|
||||||
|
// throw new UnauthorizedAccessException("Unknown user claims.");
|
||||||
|
// }
|
||||||
|
// return user;
|
||||||
|
//}
|
||||||
|
}
|
||||||
108
Shogi.Sockets/Controllers/UserController.cs
Normal file
108
Shogi.Sockets/Controllers/UserController.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Shogi.Contracts.Api;
|
||||||
|
using Shogi.Api.Extensions;
|
||||||
|
using Shogi.Api.Managers;
|
||||||
|
using Shogi.Api.Models;
|
||||||
|
using Shogi.Api.Repositories;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Shogi.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class UserController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ISocketTokenCache tokenCache;
|
||||||
|
private readonly ISocketConnectionManager connectionManager;
|
||||||
|
private readonly IUserRepository userRepository;
|
||||||
|
private readonly IShogiUserClaimsTransformer claimsTransformation;
|
||||||
|
private readonly AuthenticationProperties authenticationProps;
|
||||||
|
|
||||||
|
public UserController(
|
||||||
|
ILogger<UserController> logger,
|
||||||
|
ISocketTokenCache tokenCache,
|
||||||
|
ISocketConnectionManager connectionManager,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IShogiUserClaimsTransformer claimsTransformation)
|
||||||
|
{
|
||||||
|
this.tokenCache = tokenCache;
|
||||||
|
this.connectionManager = connectionManager;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.claimsTransformation = claimsTransformation;
|
||||||
|
authenticationProps = new AuthenticationProperties
|
||||||
|
{
|
||||||
|
AllowRefresh = true,
|
||||||
|
IsPersistent = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("GuestLogout")]
|
||||||
|
public async Task<IActionResult> GuestLogout()
|
||||||
|
{
|
||||||
|
var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
var userId = User?.GetGuestUserId();
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
connectionManager.Unsubscribe(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await signoutTask;
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
//[HttpGet("Token")]
|
||||||
|
//public async Task<IActionResult> GetToken()
|
||||||
|
//{
|
||||||
|
// var user = await gameboardManager.ReadUser(User);
|
||||||
|
// if (user == null)
|
||||||
|
// {
|
||||||
|
// await gameboardManager.CreateUser(User);
|
||||||
|
// user = await gameboardManager.ReadUser(User);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (user == null)
|
||||||
|
// {
|
||||||
|
// return Unauthorized();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var token = tokenCache.GenerateToken(user.Id);
|
||||||
|
// return new JsonResult(new CreateTokenResponse(token));
|
||||||
|
//}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("LoginAsGuest")]
|
||||||
|
public async Task<IActionResult> GuestLogin()
|
||||||
|
{
|
||||||
|
var principal = await this.claimsTransformation.CreateClaimsFromGuestPrincipal(User);
|
||||||
|
if (principal != null)
|
||||||
|
{
|
||||||
|
await HttpContext.SignInAsync(
|
||||||
|
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
|
principal,
|
||||||
|
authenticationProps
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
[HttpGet("GuestToken")]
|
||||||
|
public IActionResult GetGuestToken()
|
||||||
|
{
|
||||||
|
var id = User.GetGuestUserId();
|
||||||
|
var displayName = User.DisplayName();
|
||||||
|
if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(displayName))
|
||||||
|
{
|
||||||
|
var token = tokenCache.GenerateToken(User.GetGuestUserId()!);
|
||||||
|
return this.Ok(new CreateGuestTokenResponse(id, displayName, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.Unauthorized();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets
|
namespace Shogi.Api
|
||||||
{
|
{
|
||||||
namespace anonymous_session.Middlewares
|
namespace anonymous_session.Middlewares
|
||||||
{
|
{
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// TODO: Use this example in the guest session logic instead of custom claims.
|
/// TODO: Use this example in the guest session logic instead of custom claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AnonymousSessionMiddleware
|
public class ExampleAnonymousSessionMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
public AnonymousSessionMiddleware(RequestDelegate next)
|
public ExampleAnonymousSessionMiddleware(RequestDelegate next)
|
||||||
{
|
{
|
||||||
_next = next;
|
_next = next;
|
||||||
}
|
}
|
||||||
30
Shogi.Sockets/Extensions/Extensions.cs
Normal file
30
Shogi.Sockets/Extensions/Extensions.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Shogi.Api.Extensions;
|
||||||
|
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
private static readonly string MsalUsernameClaim = "preferred_username";
|
||||||
|
|
||||||
|
public static string? GetGuestUserId(this ClaimsPrincipal self)
|
||||||
|
{
|
||||||
|
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? DisplayName(this ClaimsPrincipal self)
|
||||||
|
{
|
||||||
|
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsMicrosoft(this ClaimsPrincipal self)
|
||||||
|
{
|
||||||
|
return self.HasClaim(c => c.Type == MsalUsernameClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetMicrosoftUserId(this ClaimsPrincipal self)
|
||||||
|
{
|
||||||
|
return self.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim)?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetShogiUserId(this ClaimsPrincipal self) => self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user