squash a bunch of commits
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -53,3 +53,5 @@ Thumbs.db
|
||||
bin
|
||||
obj
|
||||
*.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>
|
||||
<AnalysisLevel>5</AnalysisLevel>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||
<Title>Shogi Service Models</Title>
|
||||
<Description>Contains DTOs use for http requests to Shogi backend services.</Description>
|
||||
</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.Collections.Generic;
|
||||
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
namespace Shogi.Contracts.Types
|
||||
{
|
||||
public class BoardState
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
namespace Shogi.Contracts.Types
|
||||
{
|
||||
public class Move
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
namespace Shogi.Contracts.Types
|
||||
{
|
||||
public class Piece
|
||||
{
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||
namespace Shogi.Contracts.Types
|
||||
{
|
||||
public class Session
|
||||
{
|
||||
public string Player1 { get; set; }
|
||||
public string? Player2 { get; set; }
|
||||
public string GameName { get; set; }
|
||||
public string SessionName { 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
|
||||
{
|
||||
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
|
||||
{
|
||||
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 BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.Pieces.Piece>;
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
||||
|
||||
namespace Shogi.Domain
|
||||
{
|
||||
public class BoardState
|
||||
{
|
||||
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
||||
/// <summary>
|
||||
/// Key is position notation, such as "E4".
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Piece?> board;
|
||||
public class BoardState
|
||||
{
|
||||
/// <summary>
|
||||
/// Board state before any moves have been made, using standard setup and rules.
|
||||
/// </summary>
|
||||
public static readonly BoardState StandardStarting = new();
|
||||
|
||||
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)
|
||||
{
|
||||
board = state;
|
||||
Player1Hand = new List<Piece>();
|
||||
Player2Hand = new List<Piece>();
|
||||
PreviousMoveTo = Vector2.Zero;
|
||||
}
|
||||
public BoardState(Dictionary<string, Piece?> state)
|
||||
{
|
||||
board = state;
|
||||
Player1Hand = new List<Piece>();
|
||||
Player2Hand = new List<Piece>();
|
||||
PreviousMoveTo = Vector2.Zero;
|
||||
}
|
||||
|
||||
public BoardState()
|
||||
{
|
||||
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
||||
InitializeBoardState();
|
||||
Player1Hand = new List<Piece>();
|
||||
Player2Hand = new List<Piece>();
|
||||
PreviousMoveTo = Vector2.Zero;
|
||||
}
|
||||
public BoardState()
|
||||
{
|
||||
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
||||
InitializeBoardState();
|
||||
Player1Hand = new List<Piece>();
|
||||
Player2Hand = new List<Piece>();
|
||||
PreviousMoveTo = Vector2.Zero;
|
||||
}
|
||||
|
||||
public Dictionary<string, Piece?> State => board;
|
||||
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||
{
|
||||
var piece = kvp.Value;
|
||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1;
|
||||
}).Key);
|
||||
public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||
{
|
||||
var piece = kvp.Value;
|
||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2;
|
||||
}).Key);
|
||||
public List<Piece> Player1Hand { get; }
|
||||
public List<Piece> Player2Hand { get; }
|
||||
public Vector2 PreviousMoveFrom { get; private set; }
|
||||
public Vector2 PreviousMoveTo { get; private set; }
|
||||
public WhichPlayer WhoseTurn { get; set; }
|
||||
public WhichPlayer? InCheck { get; set; }
|
||||
public bool IsCheckmate { get; set; }
|
||||
public Dictionary<string, Piece?> State => board;
|
||||
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||
{
|
||||
var piece = kvp.Value;
|
||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1;
|
||||
}).Key);
|
||||
public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||
{
|
||||
var piece = kvp.Value;
|
||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2;
|
||||
}).Key);
|
||||
public List<Piece> Player1Hand { get; }
|
||||
public List<Piece> Player2Hand { get; }
|
||||
public Vector2 PreviousMoveFrom { get; private set; }
|
||||
public Vector2 PreviousMoveTo { get; private set; }
|
||||
public WhichPlayer WhoseTurn { get; set; }
|
||||
public WhichPlayer? InCheck { get; set; }
|
||||
public bool IsCheckmate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Copy constructor.
|
||||
/// </summary>
|
||||
public BoardState(BoardState other) : this()
|
||||
{
|
||||
foreach (var kvp in other.board)
|
||||
{
|
||||
// Replace copy constructor with static factory method in Piece.cs
|
||||
board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value);
|
||||
}
|
||||
WhoseTurn = other.WhoseTurn;
|
||||
InCheck = other.InCheck;
|
||||
IsCheckmate = other.IsCheckmate;
|
||||
PreviousMoveTo = other.PreviousMoveTo;
|
||||
Player1Hand.AddRange(other.Player1Hand);
|
||||
Player2Hand.AddRange(other.Player2Hand);
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy constructor.
|
||||
/// </summary>
|
||||
public BoardState(BoardState other) : this()
|
||||
{
|
||||
foreach (var kvp in other.board)
|
||||
{
|
||||
// Replace copy constructor with static factory method in Piece.cs
|
||||
board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value);
|
||||
}
|
||||
WhoseTurn = other.WhoseTurn;
|
||||
InCheck = other.InCheck;
|
||||
IsCheckmate = other.IsCheckmate;
|
||||
PreviousMoveTo = other.PreviousMoveTo;
|
||||
Player1Hand.AddRange(other.Player1Hand);
|
||||
Player2Hand.AddRange(other.Player2Hand);
|
||||
}
|
||||
|
||||
public Piece? this[string notation]
|
||||
{
|
||||
// TODO: Validate "notation" here and throw an exception if invalid.
|
||||
get => board[notation];
|
||||
set => board[notation] = value;
|
||||
}
|
||||
public Piece? this[string notation]
|
||||
{
|
||||
// TODO: Validate "notation" here and throw an exception if invalid.
|
||||
get => board[notation];
|
||||
set => board[notation] = value;
|
||||
}
|
||||
|
||||
public Piece? this[Vector2 vector]
|
||||
{
|
||||
get => this[Notation.ToBoardNotation(vector)];
|
||||
set => this[Notation.ToBoardNotation(vector)] = value;
|
||||
}
|
||||
public Piece? this[Vector2 vector]
|
||||
{
|
||||
get => this[Notation.ToBoardNotation(vector)];
|
||||
set => this[Notation.ToBoardNotation(vector)] = value;
|
||||
}
|
||||
|
||||
public Piece? this[int x, int y]
|
||||
{
|
||||
get => this[Notation.ToBoardNotation(x, y)];
|
||||
set => this[Notation.ToBoardNotation(x, y)] = value;
|
||||
}
|
||||
public Piece? this[int x, int y]
|
||||
{
|
||||
get => this[Notation.ToBoardNotation(x, y)];
|
||||
set => this[Notation.ToBoardNotation(x, y)] = value;
|
||||
}
|
||||
|
||||
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
|
||||
{
|
||||
PreviousMoveFrom = from;
|
||||
PreviousMoveTo = to;
|
||||
}
|
||||
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
|
||||
{
|
||||
PreviousMoveFrom = from;
|
||||
PreviousMoveTo = to;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given path can be traversed without colliding into a piece.
|
||||
/// </summary>
|
||||
public bool IsPathBlocked(IEnumerable<Vector2> path)
|
||||
{
|
||||
return !path.Any()
|
||||
|| path.SkipLast(1).Any(position => this[position] != null)
|
||||
|| this[path.Last()]?.Owner == WhoseTurn;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns true if the given path can be traversed without colliding into a piece.
|
||||
/// </summary>
|
||||
public bool IsPathBlocked(IEnumerable<Vector2> path)
|
||||
{
|
||||
return !path.Any()
|
||||
|| path.SkipLast(1).Any(position => this[position] != null)
|
||||
|| this[path.Last()]?.Owner == WhoseTurn;
|
||||
}
|
||||
|
||||
internal bool IsWithinPromotionZone(Vector2 position)
|
||||
{
|
||||
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|
||||
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
|
||||
}
|
||||
internal bool IsWithinPromotionZone(Vector2 position)
|
||||
{
|
||||
return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5)
|
||||
|| (WhoseTurn == WhichPlayer.Player2 && position.Y < 3);
|
||||
}
|
||||
|
||||
internal static bool IsWithinBoardBoundary(Vector2 position)
|
||||
{
|
||||
return position.X <= 8 && position.X >= 0
|
||||
&& position.Y <= 8 && position.Y >= 0;
|
||||
}
|
||||
internal static bool IsWithinBoardBoundary(Vector2 position)
|
||||
{
|
||||
return position.X <= 8 && position.X >= 0
|
||||
&& position.Y <= 8 && position.Y >= 0;
|
||||
}
|
||||
|
||||
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
||||
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
||||
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
||||
.ToList();
|
||||
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
||||
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
||||
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
||||
.ToList();
|
||||
|
||||
internal void Capture(Vector2 to)
|
||||
{
|
||||
var piece = this[to];
|
||||
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
|
||||
internal void Capture(Vector2 to)
|
||||
{
|
||||
var piece = this[to];
|
||||
if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist.");
|
||||
|
||||
piece.Capture(WhoseTurn);
|
||||
ActivePlayerHand.Add(piece);
|
||||
}
|
||||
piece.Capture(WhoseTurn);
|
||||
ActivePlayerHand.Add(piece);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does not include the start position.
|
||||
/// </summary>
|
||||
internal static IEnumerable<Vector2> GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction)
|
||||
{
|
||||
var next = start;
|
||||
while (IsWithinBoardBoundary(next + direction))
|
||||
{
|
||||
next += direction;
|
||||
yield return next;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Does not include the start position.
|
||||
/// </summary>
|
||||
internal static IEnumerable<Vector2> GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction)
|
||||
{
|
||||
var next = start;
|
||||
while (IsWithinBoardBoundary(next + direction))
|
||||
{
|
||||
next += direction;
|
||||
yield return next;
|
||||
}
|
||||
}
|
||||
|
||||
internal Piece? QueryFirstPieceInPath(IEnumerable<Vector2> path)
|
||||
{
|
||||
foreach (var step in path)
|
||||
{
|
||||
if (this[step] != null) return this[step];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
internal Piece? QueryFirstPieceInPath(IEnumerable<Vector2> path)
|
||||
{
|
||||
foreach (var step in path)
|
||||
{
|
||||
if (this[step] != null) return this[step];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void InitializeBoardState()
|
||||
{
|
||||
this["A1"] = new Lance(WhichPlayer.Player1);
|
||||
this["B1"] = new Knight(WhichPlayer.Player1);
|
||||
this["C1"] = new SilverGeneral(WhichPlayer.Player1);
|
||||
this["D1"] = new GoldGeneral(WhichPlayer.Player1);
|
||||
this["E1"] = new King(WhichPlayer.Player1);
|
||||
this["F1"] = new GoldGeneral(WhichPlayer.Player1);
|
||||
this["G1"] = new SilverGeneral(WhichPlayer.Player1);
|
||||
this["H1"] = new Knight(WhichPlayer.Player1);
|
||||
this["I1"] = new Lance(WhichPlayer.Player1);
|
||||
private void InitializeBoardState()
|
||||
{
|
||||
this["A1"] = new Lance(WhichPlayer.Player1);
|
||||
this["B1"] = new Knight(WhichPlayer.Player1);
|
||||
this["C1"] = new SilverGeneral(WhichPlayer.Player1);
|
||||
this["D1"] = new GoldGeneral(WhichPlayer.Player1);
|
||||
this["E1"] = new King(WhichPlayer.Player1);
|
||||
this["F1"] = new GoldGeneral(WhichPlayer.Player1);
|
||||
this["G1"] = new SilverGeneral(WhichPlayer.Player1);
|
||||
this["H1"] = new Knight(WhichPlayer.Player1);
|
||||
this["I1"] = new Lance(WhichPlayer.Player1);
|
||||
|
||||
this["A2"] = null;
|
||||
this["B2"] = new Bishop(WhichPlayer.Player1);
|
||||
this["C2"] = null;
|
||||
this["D2"] = null;
|
||||
this["E2"] = null;
|
||||
this["F2"] = null;
|
||||
this["G2"] = null;
|
||||
this["H2"] = new Rook(WhichPlayer.Player1);
|
||||
this["I2"] = null;
|
||||
this["A2"] = null;
|
||||
this["B2"] = new Bishop(WhichPlayer.Player1);
|
||||
this["C2"] = null;
|
||||
this["D2"] = null;
|
||||
this["E2"] = null;
|
||||
this["F2"] = null;
|
||||
this["G2"] = null;
|
||||
this["H2"] = new Rook(WhichPlayer.Player1);
|
||||
this["I2"] = null;
|
||||
|
||||
this["A3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["B3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["C3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["D3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["E3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["F3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["G3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["H3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["I3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["A3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["B3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["C3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["D3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["E3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["F3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["G3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["H3"] = new Pawn(WhichPlayer.Player1);
|
||||
this["I3"] = new Pawn(WhichPlayer.Player1);
|
||||
|
||||
this["A4"] = null;
|
||||
this["B4"] = null;
|
||||
this["C4"] = null;
|
||||
this["D4"] = null;
|
||||
this["E4"] = null;
|
||||
this["F4"] = null;
|
||||
this["G4"] = null;
|
||||
this["H4"] = null;
|
||||
this["I4"] = null;
|
||||
this["A4"] = null;
|
||||
this["B4"] = null;
|
||||
this["C4"] = null;
|
||||
this["D4"] = null;
|
||||
this["E4"] = null;
|
||||
this["F4"] = null;
|
||||
this["G4"] = null;
|
||||
this["H4"] = null;
|
||||
this["I4"] = null;
|
||||
|
||||
this["A5"] = null;
|
||||
this["B5"] = null;
|
||||
this["C5"] = null;
|
||||
this["D5"] = null;
|
||||
this["E5"] = null;
|
||||
this["F5"] = null;
|
||||
this["G5"] = null;
|
||||
this["H5"] = null;
|
||||
this["I5"] = null;
|
||||
this["A5"] = null;
|
||||
this["B5"] = null;
|
||||
this["C5"] = null;
|
||||
this["D5"] = null;
|
||||
this["E5"] = null;
|
||||
this["F5"] = null;
|
||||
this["G5"] = null;
|
||||
this["H5"] = null;
|
||||
this["I5"] = null;
|
||||
|
||||
this["A6"] = null;
|
||||
this["B6"] = null;
|
||||
this["C6"] = null;
|
||||
this["D6"] = null;
|
||||
this["E6"] = null;
|
||||
this["F6"] = null;
|
||||
this["G6"] = null;
|
||||
this["H6"] = null;
|
||||
this["I6"] = null;
|
||||
this["A6"] = null;
|
||||
this["B6"] = null;
|
||||
this["C6"] = null;
|
||||
this["D6"] = null;
|
||||
this["E6"] = null;
|
||||
this["F6"] = null;
|
||||
this["G6"] = null;
|
||||
this["H6"] = null;
|
||||
this["I6"] = null;
|
||||
|
||||
this["A7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["B7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["C7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["D7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["E7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["F7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["G7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["H7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["I7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["A7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["B7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["C7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["D7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["E7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["F7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["G7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["H7"] = new Pawn(WhichPlayer.Player2);
|
||||
this["I7"] = new Pawn(WhichPlayer.Player2);
|
||||
|
||||
this["A8"] = null;
|
||||
this["B8"] = new Rook(WhichPlayer.Player2);
|
||||
this["C8"] = null;
|
||||
this["D8"] = null;
|
||||
this["E8"] = null;
|
||||
this["F8"] = null;
|
||||
this["G8"] = null;
|
||||
this["H8"] = new Bishop(WhichPlayer.Player2);
|
||||
this["I8"] = null;
|
||||
this["A8"] = null;
|
||||
this["B8"] = new Rook(WhichPlayer.Player2);
|
||||
this["C8"] = null;
|
||||
this["D8"] = null;
|
||||
this["E8"] = null;
|
||||
this["F8"] = null;
|
||||
this["G8"] = null;
|
||||
this["H8"] = new Bishop(WhichPlayer.Player2);
|
||||
this["I8"] = null;
|
||||
|
||||
this["A9"] = new Lance(WhichPlayer.Player2);
|
||||
this["B9"] = new Knight(WhichPlayer.Player2);
|
||||
this["C9"] = new SilverGeneral(WhichPlayer.Player2);
|
||||
this["D9"] = new GoldGeneral(WhichPlayer.Player2);
|
||||
this["E9"] = new King(WhichPlayer.Player2);
|
||||
this["F9"] = new GoldGeneral(WhichPlayer.Player2);
|
||||
this["G9"] = new SilverGeneral(WhichPlayer.Player2);
|
||||
this["H9"] = new Knight(WhichPlayer.Player2);
|
||||
this["I9"] = new Lance(WhichPlayer.Player2);
|
||||
}
|
||||
}
|
||||
this["A9"] = new Lance(WhichPlayer.Player2);
|
||||
this["B9"] = new Knight(WhichPlayer.Player2);
|
||||
this["C9"] = new SilverGeneral(WhichPlayer.Player2);
|
||||
this["D9"] = new GoldGeneral(WhichPlayer.Player2);
|
||||
this["E9"] = new King(WhichPlayer.Player2);
|
||||
this["F9"] = new GoldGeneral(WhichPlayer.Player2);
|
||||
this["G9"] = new SilverGeneral(WhichPlayer.Player2);
|
||||
this["H9"] = new Knight(WhichPlayer.Player2);
|
||||
this["I9"] = new Lance(WhichPlayer.Player2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Shogi.Domain.Pieces;
|
||||
using Shogi.Domain.ValueObjects;
|
||||
|
||||
namespace Shogi.Domain
|
||||
{
|
||||
internal static class DomainExtensions
|
||||
internal static class DomainExtensions
|
||||
{
|
||||
public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King;
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
using Shogi.Domain.Pieces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Diagnostics;
|
||||
|
||||
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>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System"/>
|
||||
<Using Include="System.Collections.Generic"/>
|
||||
<Using Include="System.Linq"/>
|
||||
<Using Include="System.Numerics"/>
|
||||
<Using Include="System" />
|
||||
<Using Include="System.Collections.Generic" />
|
||||
<Using Include="System.Linq" />
|
||||
<Using Include="System.Numerics" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Entities\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Shogi.Domain.Pathing;
|
||||
using Shogi.Domain.Pieces;
|
||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.Pieces.Piece>;
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
||||
|
||||
namespace Shogi.Domain
|
||||
{
|
||||
internal class StandardRules
|
||||
internal class StandardRules
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -9,11 +9,11 @@
|
||||
/// <summary>
|
||||
/// TODO: Use this example in the guest session logic instead of custom claims.
|
||||
/// </summary>
|
||||
public class AnonymousSessionMiddleware
|
||||
public class ExampleAnonymousSessionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public AnonymousSessionMiddleware(RequestDelegate next)
|
||||
public ExampleAnonymousSessionMiddleware(RequestDelegate 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