checkpoint
This commit is contained in:
@@ -1,16 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|
||||||
{
|
|
||||||
public class GetSessionResponse
|
|
||||||
{
|
|
||||||
public Game Game { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The perspective on the game of the requesting user.
|
|
||||||
/// </summary>
|
|
||||||
public WhichPerspective PlayerPerspective { get; set; }
|
|
||||||
public BoardState BoardState { get; set; }
|
|
||||||
public IList<Move> MoveHistory { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
|||||||
{
|
{
|
||||||
public class GetSessionsResponse
|
public class GetSessionsResponse
|
||||||
{
|
{
|
||||||
public Collection<Game> PlayerHasJoinedSessions { get; set; }
|
public Collection<Session> PlayerHasJoinedSessions { get; set; }
|
||||||
public Collection<Game> AllOtherSessions { get; set; }
|
public Collection<Session> AllOtherSessions { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
|||||||
public class CreateGameResponse : IResponse
|
public class CreateGameResponse : IResponse
|
||||||
{
|
{
|
||||||
public string Action { get; }
|
public string Action { get; }
|
||||||
public Game Game { get; set; }
|
public Session Game { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The player who created the game.
|
/// The player who created the game.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
|||||||
public Dictionary<string, Piece?> Board { get; set; } = new Dictionary<string, Piece?>();
|
public Dictionary<string, Piece?> Board { get; set; } = new Dictionary<string, Piece?>();
|
||||||
public IReadOnlyCollection<Piece> Player1Hand { get; set; } = Array.Empty<Piece>();
|
public IReadOnlyCollection<Piece> Player1Hand { get; set; } = Array.Empty<Piece>();
|
||||||
public IReadOnlyCollection<Piece> Player2Hand { get; set; } = Array.Empty<Piece>();
|
public IReadOnlyCollection<Piece> Player2Hand { get; set; } = Array.Empty<Piece>();
|
||||||
public WhichPerspective? PlayerInCheck { get; set; }
|
public WhichPlayer? PlayerInCheck { get; set; }
|
||||||
public WhichPerspective WhoseTurn { get; set; }
|
public WhichPlayer WhoseTurn { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
|
||||||
{
|
|
||||||
public class Game
|
|
||||||
{
|
|
||||||
public string Player1 { get; set; }
|
|
||||||
public string? Player2 { get; set; }
|
|
||||||
public string GameName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Players[0] is the session owner, Players[1] is the other person.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<string> Players
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var list = new List<string>(2) { Player1 };
|
|
||||||
if (!string.IsNullOrEmpty(Player2)) list.Add(Player2);
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructor for serialization.
|
|
||||||
/// </summary>
|
|
||||||
public Game()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Game(string gameName, string player1, string? player2 = null)
|
|
||||||
{
|
|
||||||
GameName = gameName;
|
|
||||||
Player1 = player1;
|
|
||||||
Player2 = player2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
{
|
{
|
||||||
public bool IsPromoted { get; set; }
|
public bool IsPromoted { get; set; }
|
||||||
public WhichPiece WhichPiece { get; set; }
|
public WhichPiece WhichPiece { get; set; }
|
||||||
public WhichPerspective Owner { get; set; }
|
public WhichPlayer Owner { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
Gameboard.ShogiUI.Sockets.ServiceModels/Types/Session.cs
Normal file
10
Gameboard.ShogiUI.Sockets.ServiceModels/Types/Session.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||||
|
{
|
||||||
|
public class Session
|
||||||
|
{
|
||||||
|
public string Player1 { get; set; }
|
||||||
|
public string? Player2 { get; set; }
|
||||||
|
public string GameName { get; set; }
|
||||||
|
public BoardState BoardState { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||||
{
|
{
|
||||||
public enum WhichPerspective
|
public enum WhichPlayer
|
||||||
{
|
{
|
||||||
Player1,
|
Player1,
|
||||||
Player2,
|
Player2
|
||||||
Spectator
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -43,10 +41,6 @@ Global
|
|||||||
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
using Gameboard.ShogiUI.Sockets.Managers;
|
||||||
using Gameboard.ShogiUI.Sockets.Managers;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||||
@@ -7,10 +6,8 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||||
@@ -169,25 +166,48 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerPerspective = WhichPerspective.Spectator;
|
var playerPerspective = session.Player2Name == user.Id
|
||||||
if (session.Player1.Id == user.Id)
|
? WhichPlayer.Player2
|
||||||
{
|
: WhichPlayer.Player1;
|
||||||
playerPerspective = WhichPerspective.Player1;
|
|
||||||
}
|
|
||||||
else if (session.Player2?.Id == user.Id)
|
|
||||||
{
|
|
||||||
playerPerspective = WhichPerspective.Player2;
|
|
||||||
}
|
|
||||||
|
|
||||||
communicationManager.SubscribeToGame(session, user!.Id);
|
var response = new Session
|
||||||
var response = new GetSessionResponse()
|
|
||||||
{
|
{
|
||||||
Game = new Models.SessionMetadata(session).ToServiceModel(),
|
BoardState = new BoardState
|
||||||
BoardState = session.Shogi.ToServiceModel(),
|
{
|
||||||
MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(),
|
Board = null,
|
||||||
PlayerPerspective = playerPerspective
|
Player1Hand = session.Player1Hand.Select(MapPiece).ToList(),
|
||||||
|
Player2Hand = session.Player2Hand.Select(MapPiece).ToList(),
|
||||||
|
PlayerInCheck = session.InCheck.HasValue ? Map(session.InCheck.Value) : null
|
||||||
|
},
|
||||||
|
GameName = session.Name,
|
||||||
|
Player1 = session.Player1Name,
|
||||||
|
Player2 = session.Player2Name
|
||||||
};
|
};
|
||||||
return new JsonResult(response);
|
return this.Ok(response);
|
||||||
|
|
||||||
|
static WhichPlayer Map(Shogi.Domain.WhichPlayer whichPlayer)
|
||||||
|
{
|
||||||
|
return whichPlayer == Shogi.Domain.WhichPlayer.Player1
|
||||||
|
? WhichPlayer.Player1
|
||||||
|
: WhichPlayer.Player2;
|
||||||
|
}
|
||||||
|
static Piece MapPiece(Shogi.Domain.Pieces.Piece piece)
|
||||||
|
{
|
||||||
|
var owner = Map(piece.Owner);
|
||||||
|
var whichPiece = piece.WhichPiece switch
|
||||||
|
{
|
||||||
|
Shogi.Domain.WhichPiece.King => WhichPiece.King,
|
||||||
|
Shogi.Domain.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral,
|
||||||
|
Shogi.Domain.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral,
|
||||||
|
Shogi.Domain.WhichPiece.Bishop => WhichPiece.Bishop,
|
||||||
|
Shogi.Domain.WhichPiece.Rook => WhichPiece.Rook,
|
||||||
|
Shogi.Domain.WhichPiece.Knight => WhichPiece.Knight,
|
||||||
|
Shogi.Domain.WhichPiece.Lance => WhichPiece.Lance,
|
||||||
|
Shogi.Domain.WhichPiece.Pawn => WhichPiece.Pawn,
|
||||||
|
_ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}")
|
||||||
|
};
|
||||||
|
return new Piece { IsPromoted = piece.IsPromoted, Owner = owner, WhichPiece = whichPiece };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -207,8 +227,8 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
|
|
||||||
return new GetSessionsResponse
|
return new GetSessionsResponse
|
||||||
{
|
{
|
||||||
PlayerHasJoinedSessions = new Collection<Game>(sessionsJoinedByUser),
|
PlayerHasJoinedSessions = new Collection<Session>(sessionsJoinedByUser),
|
||||||
AllOtherSessions = new Collection<Game>(sessionsNotJoinedByUser)
|
AllOtherSessions = new Collection<Session>(sessionsNotJoinedByUser)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
using Gameboard.ShogiUI.Sockets.Managers;
|
using Gameboard.ShogiUI.Sockets.Managers;
|
||||||
using Gameboard.ShogiUI.Sockets.Models;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
@@ -56,7 +55,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
|
|||||||
var userId = User?.UserId();
|
var userId = User?.UserId();
|
||||||
if (!string.IsNullOrEmpty(userId))
|
if (!string.IsNullOrEmpty(userId))
|
||||||
{
|
{
|
||||||
connectionManager.UnsubscribeFromBroadcastAndGames(userId);
|
connectionManager.Unsubscribe(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await signoutTask;
|
await signoutTask;
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Extensions
|
|
||||||
{
|
|
||||||
public static class ModelExtensions
|
|
||||||
{
|
|
||||||
public static string GetShortName(this Models.Piece self)
|
|
||||||
{
|
|
||||||
var name = self.WhichPiece switch
|
|
||||||
{
|
|
||||||
WhichPiece.King => " K ",
|
|
||||||
WhichPiece.GoldGeneral => " G ",
|
|
||||||
WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ",
|
|
||||||
WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ",
|
|
||||||
WhichPiece.Rook => self.IsPromoted ? "^R " : " R ",
|
|
||||||
WhichPiece.Knight => self.IsPromoted ? "^k " : " k ",
|
|
||||||
WhichPiece.Lance => self.IsPromoted ? "^L " : " L ",
|
|
||||||
WhichPiece.Pawn => self.IsPromoted ? "^P " : " P ",
|
|
||||||
_ => " ? ",
|
|
||||||
};
|
|
||||||
if (self.Owner == WhichPerspective.Player2)
|
|
||||||
name = Regex.Replace(name, @"([^\s]+)\s", "$1.");
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string PrintStateAsAscii(this Models.Shogi self)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.Append(" Player 2(.)");
|
|
||||||
builder.AppendLine();
|
|
||||||
for (var y = 8; y >= 0; y--)
|
|
||||||
{
|
|
||||||
builder.Append("- ");
|
|
||||||
for (var x = 0; x < 8; x++) builder.Append("- - ");
|
|
||||||
builder.Append("- -");
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.Append('|');
|
|
||||||
for (var x = 0; x < 9; x++)
|
|
||||||
{
|
|
||||||
var piece = self.Board[x, y];
|
|
||||||
if (piece == null)
|
|
||||||
{
|
|
||||||
builder.Append(" ");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
builder.AppendFormat("{0}", piece.GetShortName());
|
|
||||||
}
|
|
||||||
builder.Append('|');
|
|
||||||
}
|
|
||||||
builder.AppendLine();
|
|
||||||
}
|
|
||||||
builder.Append("- ");
|
|
||||||
for (var x = 0; x < 8; x++) builder.Append("- - ");
|
|
||||||
builder.Append("- -");
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.Append(" Player 1");
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,9 +21,12 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
|
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
|
||||||
<ProjectReference Include="..\PathFinding\PathFinding.csproj" />
|
|
||||||
<ProjectReference Include="..\Shogi.Domain\Shogi.Domain.csproj" />
|
<ProjectReference Include="..\Shogi.Domain\Shogi.Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Services\Utility\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
using Gameboard.ShogiUI.Sockets.Models;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -14,12 +13,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
public interface ISocketConnectionManager
|
public interface ISocketConnectionManager
|
||||||
{
|
{
|
||||||
Task BroadcastToAll(IResponse response);
|
Task BroadcastToAll(IResponse response);
|
||||||
//Task BroadcastToGame(string gameName, IResponse response);
|
void Subscribe(WebSocket socket, string playerName);
|
||||||
//Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2);
|
void Unsubscribe(string playerName);
|
||||||
void SubscribeToGame(Session session, string playerName);
|
|
||||||
void SubscribeToBroadcast(WebSocket socket, string playerName);
|
|
||||||
void UnsubscribeFromBroadcastAndGames(string playerName);
|
|
||||||
void UnsubscribeFromGame(string gameName, string playerName);
|
|
||||||
Task BroadcastToPlayers(IResponse response, params string?[] playerNames);
|
Task BroadcastToPlayers(IResponse response, params string?[] playerNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,59 +26,23 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
/// <summary>Dictionary key is player name.</summary>
|
/// <summary>Dictionary key is player name.</summary>
|
||||||
private readonly ConcurrentDictionary<string, WebSocket> connections;
|
private readonly ConcurrentDictionary<string, WebSocket> connections;
|
||||||
/// <summary>Dictionary key is game name.</summary>
|
/// <summary>Dictionary key is game name.</summary>
|
||||||
private readonly ConcurrentDictionary<string, Session> sessions;
|
|
||||||
private readonly ILogger<SocketConnectionManager> logger;
|
private readonly ILogger<SocketConnectionManager> logger;
|
||||||
|
|
||||||
public SocketConnectionManager(ILogger<SocketConnectionManager> logger)
|
public SocketConnectionManager(ILogger<SocketConnectionManager> logger)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
connections = new ConcurrentDictionary<string, WebSocket>();
|
connections = new ConcurrentDictionary<string, WebSocket>();
|
||||||
sessions = new ConcurrentDictionary<string, Session>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SubscribeToBroadcast(WebSocket socket, string playerName)
|
public void Subscribe(WebSocket socket, string playerName)
|
||||||
{
|
{
|
||||||
connections.TryRemove(playerName, out var _);
|
connections.TryRemove(playerName, out var _);
|
||||||
connections.TryAdd(playerName, socket);
|
connections.TryAdd(playerName, socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UnsubscribeFromBroadcastAndGames(string playerName)
|
public void Unsubscribe(string playerName)
|
||||||
{
|
{
|
||||||
connections.TryRemove(playerName, out _);
|
connections.TryRemove(playerName, out _);
|
||||||
foreach (var kvp in sessions)
|
|
||||||
{
|
|
||||||
var sessionName = kvp.Key;
|
|
||||||
UnsubscribeFromGame(sessionName, playerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unsubscribes the player from their current game, then subscribes to the new game.
|
|
||||||
/// </summary>
|
|
||||||
public void SubscribeToGame(Session session, string playerName)
|
|
||||||
{
|
|
||||||
// Unsubscribe from any other games
|
|
||||||
foreach (var kvp in sessions)
|
|
||||||
{
|
|
||||||
var gameNameKey = kvp.Key;
|
|
||||||
UnsubscribeFromGame(gameNameKey, playerName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe
|
|
||||||
if (connections.TryGetValue(playerName, out var socket))
|
|
||||||
{
|
|
||||||
var s = sessions.GetOrAdd(session.Name, session);
|
|
||||||
s.Subscriptions.TryAdd(playerName, socket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnsubscribeFromGame(string gameName, string playerName)
|
|
||||||
{
|
|
||||||
if (sessions.TryGetValue(gameName, out var s))
|
|
||||||
{
|
|
||||||
s.Subscriptions.TryRemove(playerName, out _);
|
|
||||||
if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames)
|
public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames)
|
||||||
@@ -113,15 +72,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
|
|
||||||
tasks.Add(socket.SendTextAsync(message));
|
tasks.Add(socket.SendTextAsync(message));
|
||||||
}
|
}
|
||||||
catch (WebSocketException webSocketException)
|
catch (WebSocketException)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
|
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
|
||||||
UnsubscribeFromBroadcastAndGames(kvp.Key);
|
Unsubscribe(kvp.Key);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch
|
||||||
{
|
{
|
||||||
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
|
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
|
||||||
UnsubscribeFromBroadcastAndGames(kvp.Key);
|
Unsubscribe(kvp.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
@@ -129,34 +88,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers
|
|||||||
var task = Task.WhenAll(tasks);
|
var task = Task.WhenAll(tasks);
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch
|
||||||
{
|
{
|
||||||
Console.WriteLine("Yo");
|
Console.WriteLine("Yo");
|
||||||
}
|
}
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
//public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2)
|
|
||||||
//{
|
|
||||||
// if (sessions.TryGetValue(gameName, out var session))
|
|
||||||
// {
|
|
||||||
// var serialized1 = JsonConvert.SerializeObject(forPlayer1);
|
|
||||||
// var serialized2 = JsonConvert.SerializeObject(forPlayer2);
|
|
||||||
// return Task.WhenAll(
|
|
||||||
// session.SendToPlayer1(serialized1),
|
|
||||||
// session.SendToPlayer2(serialized2));
|
|
||||||
// }
|
|
||||||
// return Task.CompletedTask;
|
|
||||||
//}
|
|
||||||
|
|
||||||
//public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers)
|
|
||||||
//{
|
|
||||||
// if (sessions.TryGetValue(gameName, out var session))
|
|
||||||
// {
|
|
||||||
// var serialized = JsonConvert.SerializeObject(messageForAllPlayers);
|
|
||||||
// return session.Broadcast(serialized);
|
|
||||||
// }
|
|
||||||
// return Task.CompletedTask;
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
using PathFinding;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Models
|
|
||||||
{
|
|
||||||
public static class MoveSets
|
|
||||||
{
|
|
||||||
public static readonly List<PathFinding.Move> King = new(8)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.Up),
|
|
||||||
new PathFinding.Move(Direction.Left),
|
|
||||||
new PathFinding.Move(Direction.Right),
|
|
||||||
new PathFinding.Move(Direction.Down),
|
|
||||||
new PathFinding.Move(Direction.UpLeft),
|
|
||||||
new PathFinding.Move(Direction.UpRight),
|
|
||||||
new PathFinding.Move(Direction.DownLeft),
|
|
||||||
new PathFinding.Move(Direction.DownRight)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> Bishop = new(4)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> PromotedBishop = new(8)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.Up),
|
|
||||||
new PathFinding.Move(Direction.Left),
|
|
||||||
new PathFinding.Move(Direction.Right),
|
|
||||||
new PathFinding.Move(Direction.Down),
|
|
||||||
new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> GoldGeneral = new(6)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.Up),
|
|
||||||
new PathFinding.Move(Direction.UpLeft),
|
|
||||||
new PathFinding.Move(Direction.UpRight),
|
|
||||||
new PathFinding.Move(Direction.Left),
|
|
||||||
new PathFinding.Move(Direction.Right),
|
|
||||||
new PathFinding.Move(Direction.Down)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> Knight = new(2)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.KnightLeft),
|
|
||||||
new PathFinding.Move(Direction.KnightRight)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> Lance = new(1)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.Up, Distance.MultiStep),
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> Pawn = new(1)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.Up)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> Rook = new(4)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.Up, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.Left, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.Right, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.Down, Distance.MultiStep)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> PromotedRook = new(8)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.Up, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.Left, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.Right, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.Down, Distance.MultiStep),
|
|
||||||
new PathFinding.Move(Direction.UpLeft),
|
|
||||||
new PathFinding.Move(Direction.UpRight),
|
|
||||||
new PathFinding.Move(Direction.DownLeft),
|
|
||||||
new PathFinding.Move(Direction.DownRight)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly List<PathFinding.Move> SilverGeneral = new(4)
|
|
||||||
{
|
|
||||||
new PathFinding.Move(Direction.Up),
|
|
||||||
new PathFinding.Move(Direction.UpLeft),
|
|
||||||
new PathFinding.Move(Direction.UpRight),
|
|
||||||
new PathFinding.Move(Direction.DownLeft),
|
|
||||||
new PathFinding.Move(Direction.DownRight)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using PathFinding;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Models
|
|
||||||
{
|
|
||||||
[DebuggerDisplay("{WhichPiece} {Owner}")]
|
|
||||||
public class Piece : IPlanarElement
|
|
||||||
{
|
|
||||||
public WhichPiece WhichPiece { get; }
|
|
||||||
public WhichPerspective Owner { get; private set; }
|
|
||||||
public bool IsPromoted { get; private set; }
|
|
||||||
public bool IsUpsideDown => Owner == WhichPerspective.Player2;
|
|
||||||
|
|
||||||
public Piece(WhichPiece piece, WhichPerspective owner, bool isPromoted = false)
|
|
||||||
{
|
|
||||||
WhichPiece = piece;
|
|
||||||
Owner = owner;
|
|
||||||
IsPromoted = isPromoted;
|
|
||||||
}
|
|
||||||
public Piece(Piece piece) : this(piece.WhichPiece, piece.Owner, piece.IsPromoted)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanPromote => !IsPromoted
|
|
||||||
&& WhichPiece != WhichPiece.King
|
|
||||||
&& WhichPiece != WhichPiece.GoldGeneral;
|
|
||||||
|
|
||||||
public void ToggleOwnership()
|
|
||||||
{
|
|
||||||
Owner = Owner == WhichPerspective.Player1
|
|
||||||
? WhichPerspective.Player2
|
|
||||||
: WhichPerspective.Player1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Promote() => IsPromoted = CanPromote;
|
|
||||||
|
|
||||||
public void Demote() => IsPromoted = false;
|
|
||||||
|
|
||||||
public void Capture()
|
|
||||||
{
|
|
||||||
ToggleOwnership();
|
|
||||||
Demote();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: There is no reason to make "new" MoveSets every time this property is accessed.
|
|
||||||
public MoveSet MoveSet => WhichPiece switch
|
|
||||||
{
|
|
||||||
WhichPiece.King => new MoveSet(this, MoveSets.King),
|
|
||||||
WhichPiece.GoldGeneral => new MoveSet(this, MoveSets.GoldGeneral),
|
|
||||||
WhichPiece.SilverGeneral => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.SilverGeneral),
|
|
||||||
WhichPiece.Bishop => new MoveSet(this, IsPromoted ? MoveSets.PromotedBishop : MoveSets.Bishop),
|
|
||||||
WhichPiece.Rook => new MoveSet(this, IsPromoted ? MoveSets.PromotedRook : MoveSets.Rook),
|
|
||||||
WhichPiece.Knight => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Knight),
|
|
||||||
WhichPiece.Lance => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Lance),
|
|
||||||
WhichPiece.Pawn => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Pawn),
|
|
||||||
_ => throw new System.NotImplementedException()
|
|
||||||
};
|
|
||||||
|
|
||||||
public ServiceModels.Types.Piece ToServiceModel()
|
|
||||||
{
|
|
||||||
return new ServiceModels.Types.Piece
|
|
||||||
{
|
|
||||||
IsPromoted = IsPromoted,
|
|
||||||
Owner = Owner,
|
|
||||||
WhichPiece = WhichPiece
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Models
|
|
||||||
{
|
|
||||||
public class Session
|
|
||||||
{
|
|
||||||
// TODO: Separate subscriptions to the Session from the Session.
|
|
||||||
[JsonIgnore] public ConcurrentDictionary<string, WebSocket> Subscriptions { get; }
|
|
||||||
public string Name { get; }
|
|
||||||
public User Player1 { get; }
|
|
||||||
public User? Player2 { get; private set; }
|
|
||||||
public bool IsPrivate { get; }
|
|
||||||
|
|
||||||
// TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied.
|
|
||||||
public Shogi Shogi { get; }
|
|
||||||
|
|
||||||
public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null)
|
|
||||||
{
|
|
||||||
Subscriptions = new ConcurrentDictionary<string, WebSocket>();
|
|
||||||
|
|
||||||
Name = name;
|
|
||||||
Player1 = player1;
|
|
||||||
Player2 = player2;
|
|
||||||
IsPrivate = isPrivate;
|
|
||||||
Shogi = shogi;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPlayer2(User user)
|
|
||||||
{
|
|
||||||
Player2 = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,6 +32,6 @@
|
|||||||
|
|
||||||
public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id;
|
public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id;
|
||||||
|
|
||||||
public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
|
public ServiceModels.Types.Session ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,463 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Utilities;
|
|
||||||
using PathFinding;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Models
|
|
||||||
{
|
|
||||||
/// <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 class Shogi
|
|
||||||
{
|
|
||||||
private delegate void MoveSetCallback(Piece piece, Vector2 position);
|
|
||||||
private readonly PathFinder2D<Piece> pathFinder;
|
|
||||||
private Shogi? validationBoard;
|
|
||||||
private Vector2 player1King;
|
|
||||||
private Vector2 player2King;
|
|
||||||
private List<Piece> Hand => WhoseTurn == WhichPerspective.Player1 ? Player1Hand : Player2Hand;
|
|
||||||
public List<Piece> Player1Hand { get; }
|
|
||||||
public List<Piece> Player2Hand { get; }
|
|
||||||
public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method
|
|
||||||
public List<Move> MoveHistory { get; }
|
|
||||||
public WhichPerspective WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPerspective.Player1 : WhichPerspective.Player2;
|
|
||||||
public WhichPerspective? InCheck { get; private set; }
|
|
||||||
public bool IsCheckmate { get; private set; }
|
|
||||||
|
|
||||||
public string Error { get; private set; }
|
|
||||||
|
|
||||||
public Shogi()
|
|
||||||
{
|
|
||||||
Board = new CoordsToNotationCollection();
|
|
||||||
MoveHistory = new List<Move>(20);
|
|
||||||
Player1Hand = new List<Piece>();
|
|
||||||
Player2Hand = new List<Piece>();
|
|
||||||
pathFinder = new PathFinder2D<Piece>(Board, 9, 9);
|
|
||||||
player1King = new Vector2(4, 0);
|
|
||||||
player2King = new Vector2(4, 8);
|
|
||||||
Error = string.Empty;
|
|
||||||
|
|
||||||
InitializeBoardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Shogi(IList<Move> moves) : this()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < moves.Count; i++)
|
|
||||||
{
|
|
||||||
if (!Move(moves[i]))
|
|
||||||
{
|
|
||||||
// Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc.
|
|
||||||
throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Shogi(Shogi toCopy)
|
|
||||||
{
|
|
||||||
Board = new CoordsToNotationCollection();
|
|
||||||
foreach (var kvp in toCopy.Board)
|
|
||||||
{
|
|
||||||
Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pathFinder = new PathFinder2D<Piece>(Board, 9, 9);
|
|
||||||
MoveHistory = new List<Move>(toCopy.MoveHistory);
|
|
||||||
Player1Hand = new List<Piece>(toCopy.Player1Hand);
|
|
||||||
Player2Hand = new List<Piece>(toCopy.Player2Hand);
|
|
||||||
player1King = toCopy.player1King;
|
|
||||||
player2King = toCopy.player2King;
|
|
||||||
Error = toCopy.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Move(Move move)
|
|
||||||
{
|
|
||||||
var otherPlayer = WhoseTurn == WhichPerspective.Player1 ? WhichPerspective.Player2 : WhichPerspective.Player1;
|
|
||||||
var moveSuccess = TryMove(move);
|
|
||||||
|
|
||||||
if (!moveSuccess)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate check
|
|
||||||
if (EvaluateCheckAfterMove(move, otherPlayer))
|
|
||||||
{
|
|
||||||
InCheck = otherPlayer;
|
|
||||||
IsCheckmate = EvaluateCheckmate();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
InCheck = null;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts a given move. Returns false if the move is illegal.
|
|
||||||
/// </summary>
|
|
||||||
private bool TryMove(Move move)
|
|
||||||
{
|
|
||||||
// Try making the move in a "throw away" board.
|
|
||||||
if (validationBoard == null)
|
|
||||||
{
|
|
||||||
validationBoard = new Shogi(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
var isValid = move.PieceFromHand.HasValue
|
|
||||||
? validationBoard.PlaceFromHand(move)
|
|
||||||
: validationBoard.PlaceFromBoard(move);
|
|
||||||
if (!isValid)
|
|
||||||
{
|
|
||||||
// Surface the error description.
|
|
||||||
Error = validationBoard.Error;
|
|
||||||
// Invalidate the "throw away" board.
|
|
||||||
validationBoard = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// If already in check, assert the move that resulted in check no longer results in check.
|
|
||||||
if (InCheck == WhoseTurn)
|
|
||||||
{
|
|
||||||
if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn))
|
|
||||||
{
|
|
||||||
// Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The move is valid and legal; update board state.
|
|
||||||
if (move.PieceFromHand.HasValue) PlaceFromHand(move);
|
|
||||||
else PlaceFromBoard(move);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/// <returns>True if the move was successful.</returns>
|
|
||||||
private bool PlaceFromHand(Move move)
|
|
||||||
{
|
|
||||||
var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand);
|
|
||||||
if (index < 0)
|
|
||||||
{
|
|
||||||
Error = $"{move.PieceFromHand} does not exist in the hand.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Board[move.To] != null)
|
|
||||||
{
|
|
||||||
Error = $"Illegal move - attempting to capture while playing a piece from the hand.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (move.PieceFromHand!.Value)
|
|
||||||
{
|
|
||||||
case WhichPiece.Knight:
|
|
||||||
{
|
|
||||||
// Knight cannot be placed onto the farthest two ranks from the hand.
|
|
||||||
if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y > 6)
|
|
||||||
|| (WhoseTurn == WhichPerspective.Player2 && move.To.Y < 2))
|
|
||||||
{
|
|
||||||
Error = $"Knight has no valid moves after placed.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WhichPiece.Lance:
|
|
||||||
case WhichPiece.Pawn:
|
|
||||||
{
|
|
||||||
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
|
|
||||||
if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y == 8)
|
|
||||||
|| (WhoseTurn == WhichPerspective.Player2 && move.To.Y == 0))
|
|
||||||
{
|
|
||||||
Error = $"{move.PieceFromHand} has no valid moves after placed.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutate the board.
|
|
||||||
Board[move.To] = Hand[index];
|
|
||||||
Hand.RemoveAt(index);
|
|
||||||
MoveHistory.Add(move);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/// <returns>True if the move was successful.</returns>
|
|
||||||
private bool PlaceFromBoard(Move move)
|
|
||||||
{
|
|
||||||
var fromPiece = Board[move.From!.Value];
|
|
||||||
if (fromPiece == null)
|
|
||||||
{
|
|
||||||
Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}.";
|
|
||||||
return false; // Invalid move
|
|
||||||
}
|
|
||||||
if (fromPiece.Owner != WhoseTurn)
|
|
||||||
{
|
|
||||||
Error = "Not allowed to move the opponents piece";
|
|
||||||
return false; // Invalid move; cannot move other players pieces.
|
|
||||||
}
|
|
||||||
if (IsPathable(move.From.Value, move.To) == false)
|
|
||||||
{
|
|
||||||
Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set.";
|
|
||||||
return false; // Invalid move; move not part of move-set.
|
|
||||||
}
|
|
||||||
|
|
||||||
var captured = Board[move.To];
|
|
||||||
if (captured != null)
|
|
||||||
{
|
|
||||||
if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece.
|
|
||||||
captured.Capture();
|
|
||||||
Hand.Add(captured);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Mutate the board.
|
|
||||||
if (move.IsPromotion)
|
|
||||||
{
|
|
||||||
if (WhoseTurn == WhichPerspective.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5))
|
|
||||||
{
|
|
||||||
fromPiece.Promote();
|
|
||||||
}
|
|
||||||
else if (WhoseTurn == WhichPerspective.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3))
|
|
||||||
{
|
|
||||||
fromPiece.Promote();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Board[move.To] = fromPiece;
|
|
||||||
Board[move.From!.Value] = null;
|
|
||||||
if (fromPiece.WhichPiece == WhichPiece.King)
|
|
||||||
{
|
|
||||||
if (fromPiece.Owner == WhichPerspective.Player1)
|
|
||||||
{
|
|
||||||
player1King.X = move.To.X;
|
|
||||||
player1King.Y = move.To.Y;
|
|
||||||
}
|
|
||||||
else if (fromPiece.Owner == WhichPerspective.Player2)
|
|
||||||
{
|
|
||||||
player2King.X = move.To.X;
|
|
||||||
player2King.Y = move.To.Y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MoveHistory.Add(move);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsPathable(Vector2 from, Vector2 to)
|
|
||||||
{
|
|
||||||
var piece = Board[from];
|
|
||||||
if (piece == null) return false;
|
|
||||||
|
|
||||||
var isObstructed = false;
|
|
||||||
var isPathable = pathFinder.PathTo(from, to, (other, position) =>
|
|
||||||
{
|
|
||||||
if (other.Owner == piece.Owner) isObstructed = true;
|
|
||||||
});
|
|
||||||
return !isObstructed && isPathable;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Rules Validation
|
|
||||||
private bool EvaluateCheckAfterMove(Move move, WhichPerspective WhichPerspective)
|
|
||||||
{
|
|
||||||
if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother.
|
|
||||||
|
|
||||||
var isCheck = false;
|
|
||||||
var kingPosition = WhichPerspective == WhichPerspective.Player1 ? player1King : player2King;
|
|
||||||
|
|
||||||
// Check if the move put the king in check.
|
|
||||||
if (pathFinder.PathTo(move.To, kingPosition)) return true;
|
|
||||||
|
|
||||||
if (move.From.HasValue)
|
|
||||||
{
|
|
||||||
// Get line equation from king through the now-unoccupied location.
|
|
||||||
var direction = Vector2.Subtract(kingPosition, move.From!.Value);
|
|
||||||
var slope = Math.Abs(direction.Y / direction.X);
|
|
||||||
// If absolute slope is 45°, look for a bishop along the line.
|
|
||||||
// If absolute slope is 0° or 90°, look for a rook along the line.
|
|
||||||
// if absolute slope is 0°, look for lance along the line.
|
|
||||||
if (float.IsInfinity(slope))
|
|
||||||
{
|
|
||||||
// if slope of the move is also infinity...can skip this?
|
|
||||||
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
|
|
||||||
{
|
|
||||||
if (piece.Owner != WhichPerspective)
|
|
||||||
{
|
|
||||||
switch (piece.WhichPiece)
|
|
||||||
{
|
|
||||||
case WhichPiece.Rook:
|
|
||||||
isCheck = true;
|
|
||||||
break;
|
|
||||||
case WhichPiece.Lance:
|
|
||||||
if (!piece.IsPromoted) isCheck = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (slope == 1)
|
|
||||||
{
|
|
||||||
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
|
|
||||||
{
|
|
||||||
if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop)
|
|
||||||
{
|
|
||||||
isCheck = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (slope == 0)
|
|
||||||
{
|
|
||||||
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
|
|
||||||
{
|
|
||||||
if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook)
|
|
||||||
{
|
|
||||||
isCheck = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent.
|
|
||||||
// Go read the shogi rules to be sure this is true.
|
|
||||||
}
|
|
||||||
|
|
||||||
return isCheck;
|
|
||||||
}
|
|
||||||
private bool EvaluateCheckmate()
|
|
||||||
{
|
|
||||||
if (!InCheck.HasValue) return false;
|
|
||||||
|
|
||||||
// Assume true and try to disprove.
|
|
||||||
var isCheckmate = true;
|
|
||||||
Board.ForEachNotNull((piece, from) => // For each piece...
|
|
||||||
{
|
|
||||||
// Short circuit
|
|
||||||
if (!isCheckmate) return;
|
|
||||||
|
|
||||||
if (piece.Owner == InCheck) // ...owned by the player in check...
|
|
||||||
{
|
|
||||||
// ...evaluate if any move gets the player out of check.
|
|
||||||
pathFinder.PathEvery(from, (other, position) =>
|
|
||||||
{
|
|
||||||
if (validationBoard == null) validationBoard = new Shogi(this);
|
|
||||||
var moveToTry = new Move(from, position);
|
|
||||||
var moveSuccess = validationBoard.TryMove(moveToTry);
|
|
||||||
if (moveSuccess)
|
|
||||||
{
|
|
||||||
validationBoard = null;
|
|
||||||
if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value))
|
|
||||||
{
|
|
||||||
isCheckmate = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return isCheckmate;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private void InitializeBoardState()
|
|
||||||
{
|
|
||||||
Board["A1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1);
|
|
||||||
Board["B1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1);
|
|
||||||
Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1);
|
|
||||||
Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1);
|
|
||||||
Board["E1"] = new Piece(WhichPiece.King, WhichPerspective.Player1);
|
|
||||||
Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1);
|
|
||||||
Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1);
|
|
||||||
Board["H1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1);
|
|
||||||
Board["I1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1);
|
|
||||||
|
|
||||||
Board["A2"] = null;
|
|
||||||
Board["B2"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player1);
|
|
||||||
Board["C2"] = null;
|
|
||||||
Board["D2"] = null;
|
|
||||||
Board["E2"] = null;
|
|
||||||
Board["F2"] = null;
|
|
||||||
Board["G2"] = null;
|
|
||||||
Board["H2"] = new Piece(WhichPiece.Rook, WhichPerspective.Player1);
|
|
||||||
Board["I2"] = null;
|
|
||||||
|
|
||||||
Board["A3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
Board["B3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
Board["C3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
Board["D3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
Board["E3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
Board["F3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
Board["G3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
Board["H3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
Board["I3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
|
||||||
|
|
||||||
Board["A4"] = null;
|
|
||||||
Board["B4"] = null;
|
|
||||||
Board["C4"] = null;
|
|
||||||
Board["D4"] = null;
|
|
||||||
Board["E4"] = null;
|
|
||||||
Board["F4"] = null;
|
|
||||||
Board["G4"] = null;
|
|
||||||
Board["H4"] = null;
|
|
||||||
Board["I4"] = null;
|
|
||||||
|
|
||||||
Board["A5"] = null;
|
|
||||||
Board["B5"] = null;
|
|
||||||
Board["C5"] = null;
|
|
||||||
Board["D5"] = null;
|
|
||||||
Board["E5"] = null;
|
|
||||||
Board["F5"] = null;
|
|
||||||
Board["G5"] = null;
|
|
||||||
Board["H5"] = null;
|
|
||||||
Board["I5"] = null;
|
|
||||||
|
|
||||||
Board["A6"] = null;
|
|
||||||
Board["B6"] = null;
|
|
||||||
Board["C6"] = null;
|
|
||||||
Board["D6"] = null;
|
|
||||||
Board["E6"] = null;
|
|
||||||
Board["F6"] = null;
|
|
||||||
Board["G6"] = null;
|
|
||||||
Board["H6"] = null;
|
|
||||||
Board["I6"] = null;
|
|
||||||
|
|
||||||
Board["A7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
Board["B7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
Board["C7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
Board["D7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
Board["E7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
Board["F7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
Board["G7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
Board["H7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
Board["I7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
|
||||||
|
|
||||||
Board["A8"] = null;
|
|
||||||
Board["B8"] = new Piece(WhichPiece.Rook, WhichPerspective.Player2);
|
|
||||||
Board["C8"] = null;
|
|
||||||
Board["D8"] = null;
|
|
||||||
Board["E8"] = null;
|
|
||||||
Board["F8"] = null;
|
|
||||||
Board["G8"] = null;
|
|
||||||
Board["H8"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player2);
|
|
||||||
Board["I8"] = null;
|
|
||||||
|
|
||||||
Board["A9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2);
|
|
||||||
Board["B9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2);
|
|
||||||
Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2);
|
|
||||||
Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2);
|
|
||||||
Board["E9"] = new Piece(WhichPiece.King, WhichPerspective.Player2);
|
|
||||||
Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2);
|
|
||||||
Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2);
|
|
||||||
Board["H9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2);
|
|
||||||
Board["I9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BoardState ToServiceModel()
|
|
||||||
{
|
|
||||||
return new BoardState
|
|
||||||
{
|
|
||||||
Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()),
|
|
||||||
PlayerInCheck = InCheck,
|
|
||||||
WhoseTurn = WhoseTurn,
|
|
||||||
Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(),
|
|
||||||
Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Utilities;
|
using Shogi.Domain;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
{
|
{
|
||||||
@@ -35,31 +34,19 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
Player2Hand = Array.Empty<Piece>();
|
Player2Hand = Array.Empty<Piece>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public BoardStateDocument(string sessionName, Models.Shogi shogi)
|
public BoardStateDocument(string sessionName, Session shogi)
|
||||||
: base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState)
|
: base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState)
|
||||||
{
|
{
|
||||||
|
static Piece MapPiece(Shogi.Domain.Pieces.Piece piece)
|
||||||
|
{
|
||||||
|
return new Piece { IsPromoted = piece.IsPromoted, Owner = piece.Owner, WhichPiece = piece.WhichPiece };
|
||||||
|
}
|
||||||
|
|
||||||
Name = sessionName;
|
Name = sessionName;
|
||||||
Board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
Board = shogi.BoardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null ? null : MapPiece(kvp.Value));
|
||||||
|
|
||||||
for (var x = 0; x < 9; x++)
|
Player1Hand = shogi.Player1Hand.Select(piece => MapPiece(piece)).ToArray();
|
||||||
for (var y = 0; y < 9; y++)
|
Player2Hand = shogi.Player2Hand.Select(piece => MapPiece(piece)).ToArray();
|
||||||
{
|
|
||||||
var position = new Vector2(x, y);
|
|
||||||
var piece = shogi.Board[y, x];
|
|
||||||
|
|
||||||
if (piece != null)
|
|
||||||
{
|
|
||||||
var positionNotation = NotationHelper.ToBoardNotation(position);
|
|
||||||
Board[positionNotation] = new Piece(piece);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Player1Hand = shogi.Player1Hand.Select(model => new Piece(model)).ToArray();
|
|
||||||
Player2Hand = shogi.Player2Hand.Select(model => new Piece(model)).ToArray();
|
|
||||||
if (shogi.MoveHistory.Count > 0)
|
|
||||||
{
|
|
||||||
Move = new Move(shogi.MoveHistory[^1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
using Shogi.Domain;
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
{
|
{
|
||||||
@@ -29,28 +28,5 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
{
|
{
|
||||||
To = string.Empty;
|
To = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Move(Models.Move move)
|
|
||||||
{
|
|
||||||
if (move.From.HasValue)
|
|
||||||
{
|
|
||||||
From = ToBoardNotation(move.From.Value);
|
|
||||||
}
|
|
||||||
IsPromotion = move.IsPromotion;
|
|
||||||
To = ToBoardNotation(move.To);
|
|
||||||
PieceFromHand = move.PieceFromHand;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly char A = 'A';
|
|
||||||
private static string ToBoardNotation(Vector2 vector)
|
|
||||||
{
|
|
||||||
var file = (char)(vector.X + A);
|
|
||||||
var rank = vector.Y + 1;
|
|
||||||
return $"{file}{rank}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public Models.Move ToDomainModel() => PieceFromHand.HasValue
|
|
||||||
? new(PieceFromHand.Value, To)
|
|
||||||
: new(From!, To, IsPromotion);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,11 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
using Shogi.Domain;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
{
|
{
|
||||||
public class Piece
|
public class Piece
|
||||||
{
|
{
|
||||||
public bool IsPromoted { get; set; }
|
public bool IsPromoted { get; set; }
|
||||||
public WhichPerspective Owner { get; set; }
|
public WhichPlayer Owner { get; set; }
|
||||||
public WhichPiece WhichPiece { get; set; }
|
public WhichPiece WhichPiece { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default constructor and setters are for deserialization.
|
|
||||||
/// </summary>
|
|
||||||
public Piece()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Piece(Models.Piece piece)
|
|
||||||
{
|
|
||||||
IsPromoted = piece.IsPromoted;
|
|
||||||
Owner = piece.Owner;
|
|
||||||
WhichPiece = piece.WhichPiece;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Models.Piece ToDomainModel() => new(WhichPiece, Owner, IsPromoted);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|
||||||
{
|
{
|
||||||
public class SessionDocument : CouchDocument
|
public class SessionDocument : CouchDocument
|
||||||
{
|
{
|
||||||
@@ -8,7 +6,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
public string Player1Id { get; set; }
|
public string Player1Id { get; set; }
|
||||||
public string? Player2Id { get; set; }
|
public string? Player2Id { get; set; }
|
||||||
public bool IsPrivate { get; set; }
|
public bool IsPrivate { get; set; }
|
||||||
public IList<BoardStateDocument> History { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default constructor and setters are for deserialization.
|
/// Default constructor and setters are for deserialization.
|
||||||
@@ -18,17 +15,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
Name = string.Empty;
|
Name = string.Empty;
|
||||||
Player1Id = string.Empty;
|
Player1Id = string.Empty;
|
||||||
Player2Id = string.Empty;
|
Player2Id = string.Empty;
|
||||||
History = new List<BoardStateDocument>(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SessionDocument(Models.Session session)
|
|
||||||
: base(session.Name, WhichDocumentType.Session)
|
|
||||||
{
|
|
||||||
Name = session.Name;
|
|
||||||
Player1Id = session.Player1.Id;
|
|
||||||
Player2Id = session.Player2?.Id;
|
|
||||||
IsPrivate = session.IsPrivate;
|
|
||||||
History = new List<BoardStateDocument>(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionDocument(Models.SessionMetadata sessionMetaData)
|
public SessionDocument(Models.SessionMetadata sessionMetaData)
|
||||||
@@ -38,7 +24,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
|||||||
Player1Id = sessionMetaData.Player1.Id;
|
Player1Id = sessionMetaData.Player1.Id;
|
||||||
Player2Id = sessionMetaData.Player2?.Id;
|
Player2Id = sessionMetaData.Player2?.Id;
|
||||||
IsPrivate = sessionMetaData.IsPrivate;
|
IsPrivate = sessionMetaData.IsPrivate;
|
||||||
History = new List<BoardStateDocument>(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http.Extensions;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Shogi.Domain;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
@@ -11,17 +12,16 @@ using System.Linq;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories
|
namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||||
{
|
{
|
||||||
public interface IGameboardRepository
|
public interface IGameboardRepository
|
||||||
{
|
{
|
||||||
Task<bool> CreateBoardState(Models.Session session);
|
Task<bool> CreateBoardState(Session session);
|
||||||
Task<bool> CreateSession(Models.SessionMetadata session);
|
Task<bool> CreateSession(Models.SessionMetadata session);
|
||||||
Task<bool> CreateUser(Models.User user);
|
Task<bool> CreateUser(Models.User user);
|
||||||
Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas();
|
Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas();
|
||||||
Task<Models.Session?> ReadSession(string name);
|
Task<Session?> ReadSession(string name);
|
||||||
Task<bool> UpdateSession(Models.SessionMetadata session);
|
Task<bool> UpdateSession(Models.SessionMetadata session);
|
||||||
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
|
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
|
||||||
Task<Models.User?> ReadUser(string userName);
|
Task<Models.User?> ReadUser(string userName);
|
||||||
@@ -80,8 +80,15 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>());
|
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Models.Session?> ReadSession(string name)
|
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
|
var queryParams = new QueryBuilder
|
||||||
{
|
{
|
||||||
{ "include_docs", "true" },
|
{ "include_docs", "true" },
|
||||||
@@ -97,11 +104,12 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
{
|
{
|
||||||
var group = result.rows;
|
var group = result.rows;
|
||||||
/**
|
/**
|
||||||
* A group contains 3 type of elements.
|
* A group contains multiple elements.
|
||||||
* 1) The session metadata.
|
* 0) The session metadata.
|
||||||
* 2) User documents of Player1 and Player2.
|
* 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 :(
|
* 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :(
|
||||||
* 3) BoardState
|
* Everything Else) Snapshots of the boardstate after every player move.
|
||||||
*/
|
*/
|
||||||
var session = group[0].doc.ToObject<SessionDocument>();
|
var session = group[0].doc.ToObject<SessionDocument>();
|
||||||
var player1Doc = group[1].doc.ToObject<UserDocument>();
|
var player1Doc = group[1].doc.ToObject<UserDocument>();
|
||||||
@@ -109,24 +117,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
|
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
|
||||||
? group[2].doc.ToObject<UserDocument>()
|
? group[2].doc.ToObject<UserDocument>()
|
||||||
: null;
|
: null;
|
||||||
var moves = group
|
var boardState = group.Last().doc.ToObject<BoardStateDocument>();
|
||||||
.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();
|
|
||||||
|
|
||||||
var shogi = new Models.Shogi(moves);
|
if (session != null && player1Doc != null && boardState != null)
|
||||||
if (session != null && player1Doc != null)
|
|
||||||
{
|
{
|
||||||
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
|
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
|
||||||
return new Models.Session(session.Name, session.IsPrivate, shogi, new(player1Doc), player2);
|
var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1Doc.DisplayName, player2Doc?.DisplayName);
|
||||||
|
var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value)));
|
||||||
|
return new Session(shogiBoardState, metaData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -170,9 +168,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves a snapshot of board state and the most recent move.
|
/// Saves a snapshot of board state and the most recent move.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<bool> CreateBoardState(Models.Session session)
|
public async Task<bool> CreateBoardState(Session session)
|
||||||
{
|
{
|
||||||
var boardStateDocument = new BoardStateDocument(session.Name, session.Shogi);
|
Piece? MapPiece(Shogi.Domain.Pieces.Piece? piece)
|
||||||
|
{
|
||||||
|
return piece == null
|
||||||
|
? null
|
||||||
|
: new Piece { IsPromoted = piece.IsPromoted, Owner = piece.Owner, WhichPiece = piece.WhichPiece };
|
||||||
|
}
|
||||||
|
|
||||||
|
var boardStateDocument = new BoardStateDocument(session.Name, session);
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
||||||
var response = await client.PostAsync(string.Empty, content);
|
var response = await client.PostAsync(string.Empty, content);
|
||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
@@ -184,7 +189,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
|
var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
|
||||||
var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent);
|
var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent);
|
||||||
|
|
||||||
var boardStateDocument = new BoardStateDocument(session.Name, new Models.Shogi());
|
var boardStateDocument = new BoardStateDocument(session.Name, new Session());
|
||||||
var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
||||||
|
|
||||||
if ((await postSessionDocumentTask).IsSuccessStatusCode)
|
if ((await postSessionDocumentTask).IsSuccessStatusCode)
|
||||||
@@ -288,5 +293,21 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
|
|||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
|
|||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
using Gameboard.ShogiUI.Sockets.Services.Utility;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -74,7 +73,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
}
|
}
|
||||||
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
communicationManager.SubscribeToBroadcast(socket, userName);
|
communicationManager.Subscribe(socket, userName);
|
||||||
while (socket.State == WebSocketState.Open)
|
while (socket.State == WebSocketState.Open)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -82,7 +81,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
var message = await socket.ReceiveTextAsync();
|
var message = await socket.ReceiveTextAsync();
|
||||||
if (string.IsNullOrWhiteSpace(message)) continue;
|
if (string.IsNullOrWhiteSpace(message)) continue;
|
||||||
logger.LogInformation("Request \n{0}\n", message);
|
logger.LogInformation("Request \n{0}\n", message);
|
||||||
var request = JsonConvert.DeserializeObject<Request>(message);
|
var request = JsonConvert.DeserializeObject<IRequest>(message);
|
||||||
if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action))
|
if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action))
|
||||||
{
|
{
|
||||||
await socket.SendTextAsync("Error: Action not recognized.");
|
await socket.SendTextAsync("Error: Action not recognized.");
|
||||||
@@ -114,7 +113,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
|
|||||||
logger.LogInformation("Probably tried writing to a closed socket.");
|
logger.LogInformation("Probably tried writing to a closed socket.");
|
||||||
logger.LogError(ex.Message);
|
logger.LogError(ex.Message);
|
||||||
}
|
}
|
||||||
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
|
communicationManager.Unsubscribe(userName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.Utility
|
|
||||||
{
|
|
||||||
public class JsonRequest
|
|
||||||
{
|
|
||||||
public IRequest Request { get; private set; }
|
|
||||||
public string Json { get; private set; }
|
|
||||||
public JsonRequest(IRequest request, string json)
|
|
||||||
{
|
|
||||||
Request = request;
|
|
||||||
Json = json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.Utility
|
|
||||||
{
|
|
||||||
public class Request : IRequest
|
|
||||||
{
|
|
||||||
public ClientAction Action { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Services.Utility
|
|
||||||
{
|
|
||||||
public class Response : IResponse
|
|
||||||
{
|
|
||||||
public string Action { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Models;
|
|
||||||
using PathFinding;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Utilities
|
|
||||||
{
|
|
||||||
public class CoordsToNotationCollection : Dictionary<string, Piece?>, IPlanarCollection<Piece>
|
|
||||||
{
|
|
||||||
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
|
||||||
|
|
||||||
public CoordsToNotationCollection() : base(81, StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public CoordsToNotationCollection(Dictionary<string, Piece?> board) : base(board, StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Piece? this[Vector2 vector]
|
|
||||||
{
|
|
||||||
get => this[NotationHelper.ToBoardNotation(vector)];
|
|
||||||
set => this[NotationHelper.ToBoardNotation(vector)] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Piece? this[int x, int y]
|
|
||||||
{
|
|
||||||
get => this[NotationHelper.ToBoardNotation(x, y)];
|
|
||||||
set => this[NotationHelper.ToBoardNotation(x, y)] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void ForEachNotNull(ForEachDelegate callback)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < 9; x++)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < 9; y++)
|
|
||||||
{
|
|
||||||
var position = new Vector2(x, y);
|
|
||||||
var elem = this[position];
|
|
||||||
if (elem != null)
|
|
||||||
callback(elem, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Numerics;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Utilities
|
|
||||||
{
|
|
||||||
public static class NotationHelper
|
|
||||||
{
|
|
||||||
private static readonly string BoardNotationRegex = @"(?<file>[a-iA-I])(?<rank>[1-9])";
|
|
||||||
private static readonly char A = 'A';
|
|
||||||
|
|
||||||
public static string ToBoardNotation(Vector2 vector)
|
|
||||||
{
|
|
||||||
return ToBoardNotation((int)vector.X, (int)vector.Y);
|
|
||||||
}
|
|
||||||
public static string ToBoardNotation(int x, int y)
|
|
||||||
{
|
|
||||||
var file = (char)(x + A);
|
|
||||||
var rank = y + 1;
|
|
||||||
return $"{file}{rank}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Vector2 FromBoardNotation(string notation)
|
|
||||||
{
|
|
||||||
notation = notation.ToUpper();
|
|
||||||
if (Regex.IsMatch(notation, BoardNotationRegex))
|
|
||||||
{
|
|
||||||
var match = Regex.Match(notation, BoardNotationRegex);
|
|
||||||
char file = match.Groups["file"].Value[0];
|
|
||||||
int rank = int.Parse(match.Groups["rank"].Value);
|
|
||||||
return new Vector2(file - A, rank - 1);
|
|
||||||
}
|
|
||||||
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ using Gameboard.ShogiUI.Sockets.Models;
|
|||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPerspective;
|
using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPlayer;
|
||||||
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece;
|
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece;
|
||||||
namespace Gameboard.ShogiUI.UnitTests.Rules
|
namespace Gameboard.ShogiUI.UnitTests.Rules
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
using AutoFixture;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Models;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Utilities;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.xUnitTests
|
|
||||||
{
|
|
||||||
public class CoordsToNotationCollectionShould
|
|
||||||
{
|
|
||||||
private readonly Fixture fixture;
|
|
||||||
private readonly CoordsToNotationCollection collection;
|
|
||||||
public CoordsToNotationCollectionShould()
|
|
||||||
{
|
|
||||||
fixture = new Fixture();
|
|
||||||
collection = new CoordsToNotationCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TranslateCoordinatesToNotation()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
collection[0, 0] = fixture.Create<Piece>();
|
|
||||||
collection[4, 4] = fixture.Create<Piece>();
|
|
||||||
collection[8, 8] = fixture.Create<Piece>();
|
|
||||||
collection[2, 2] = fixture.Create<Piece>();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
collection["A1"].Should().BeSameAs(collection[0, 0]);
|
|
||||||
collection["E5"].Should().BeSameAs(collection[4, 4]);
|
|
||||||
collection["I9"].Should().BeSameAs(collection[8, 8]);
|
|
||||||
collection["C3"].Should().BeSameAs(collection[2, 2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Yep()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ namespace Gameboard.ShogiUI.xUnitTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void DiscardNullPLayers()
|
public void DiscardNullPLayers()
|
||||||
{
|
{
|
||||||
var game = new Game("Test", "P1", null);
|
var game = new Session("Test", "P1", null);
|
||||||
|
|
||||||
game.Players.Count.Should().Be(1);
|
game.Players.Count.Should().Be(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,621 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using FluentAssertions.Execution;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
|
||||||
using Gameboard.ShogiUI.Sockets.Models;
|
|
||||||
using System.Linq;
|
|
||||||
using Xunit;
|
|
||||||
using Xunit.Abstractions;
|
|
||||||
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece;
|
|
||||||
using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPerspective;
|
|
||||||
using ShogiModel = Gameboard.ShogiUI.Sockets.Models.Shogi;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.xUnitTests
|
|
||||||
{
|
|
||||||
public class ShogiShould
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper output;
|
|
||||||
public ShogiShould(ITestOutputHelper output)
|
|
||||||
{
|
|
||||||
this.output = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InitializeBoardState()
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
var board = new ShogiModel().Board;
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
board["A1"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
board["A1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["A1"].IsPromoted.Should().Be(false);
|
|
||||||
board["B1"].WhichPiece.Should().Be(WhichPiece.Knight);
|
|
||||||
board["B1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["B1"].IsPromoted.Should().Be(false);
|
|
||||||
board["C1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
|
|
||||||
board["C1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["C1"].IsPromoted.Should().Be(false);
|
|
||||||
board["D1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
|
|
||||||
board["D1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["D1"].IsPromoted.Should().Be(false);
|
|
||||||
board["E1"].WhichPiece.Should().Be(WhichPiece.King);
|
|
||||||
board["E1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["E1"].IsPromoted.Should().Be(false);
|
|
||||||
board["F1"].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
|
|
||||||
board["F1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["F1"].IsPromoted.Should().Be(false);
|
|
||||||
board["G1"].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
|
|
||||||
board["G1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["G1"].IsPromoted.Should().Be(false);
|
|
||||||
board["H1"].WhichPiece.Should().Be(WhichPiece.Knight);
|
|
||||||
board["H1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["H1"].IsPromoted.Should().Be(false);
|
|
||||||
board["I1"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
board["I1"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["I1"].IsPromoted.Should().Be(false);
|
|
||||||
|
|
||||||
board["A2"].Should().BeNull();
|
|
||||||
board["B2"].WhichPiece.Should().Be(WhichPiece.Bishop);
|
|
||||||
board["B2"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["B2"].IsPromoted.Should().Be(false);
|
|
||||||
board["C2"].Should().BeNull();
|
|
||||||
board["D2"].Should().BeNull();
|
|
||||||
board["E2"].Should().BeNull();
|
|
||||||
board["F2"].Should().BeNull();
|
|
||||||
board["G2"].Should().BeNull();
|
|
||||||
board["H2"].WhichPiece.Should().Be(WhichPiece.Rook);
|
|
||||||
board["H2"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["H2"].IsPromoted.Should().Be(false);
|
|
||||||
board["I2"].Should().BeNull();
|
|
||||||
|
|
||||||
board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["A3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["A3"].IsPromoted.Should().Be(false);
|
|
||||||
board["B3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["B3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["B3"].IsPromoted.Should().Be(false);
|
|
||||||
board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["C3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["C3"].IsPromoted.Should().Be(false);
|
|
||||||
board["D3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["D3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["D3"].IsPromoted.Should().Be(false);
|
|
||||||
board["E3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["E3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["E3"].IsPromoted.Should().Be(false);
|
|
||||||
board["F3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["F3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["F3"].IsPromoted.Should().Be(false);
|
|
||||||
board["G3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["G3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["G3"].IsPromoted.Should().Be(false);
|
|
||||||
board["H3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["H3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["H3"].IsPromoted.Should().Be(false);
|
|
||||||
board["I3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["I3"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
board["I3"].IsPromoted.Should().Be(false);
|
|
||||||
|
|
||||||
board["A4"].Should().BeNull();
|
|
||||||
board["B4"].Should().BeNull();
|
|
||||||
board["C4"].Should().BeNull();
|
|
||||||
board["D4"].Should().BeNull();
|
|
||||||
board["E4"].Should().BeNull();
|
|
||||||
board["F4"].Should().BeNull();
|
|
||||||
board["G4"].Should().BeNull();
|
|
||||||
board["H4"].Should().BeNull();
|
|
||||||
board["I4"].Should().BeNull();
|
|
||||||
|
|
||||||
board["A5"].Should().BeNull();
|
|
||||||
board["B5"].Should().BeNull();
|
|
||||||
board["C5"].Should().BeNull();
|
|
||||||
board["D5"].Should().BeNull();
|
|
||||||
board["E5"].Should().BeNull();
|
|
||||||
board["F5"].Should().BeNull();
|
|
||||||
board["G5"].Should().BeNull();
|
|
||||||
board["H5"].Should().BeNull();
|
|
||||||
board["I5"].Should().BeNull();
|
|
||||||
|
|
||||||
board["A6"].Should().BeNull();
|
|
||||||
board["B6"].Should().BeNull();
|
|
||||||
board["C6"].Should().BeNull();
|
|
||||||
board["D6"].Should().BeNull();
|
|
||||||
board["E6"].Should().BeNull();
|
|
||||||
board["F6"].Should().BeNull();
|
|
||||||
board["G6"].Should().BeNull();
|
|
||||||
board["H6"].Should().BeNull();
|
|
||||||
board["I6"].Should().BeNull();
|
|
||||||
|
|
||||||
board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["A7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["A7"].IsPromoted.Should().Be(false);
|
|
||||||
board["B7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["B7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["B7"].IsPromoted.Should().Be(false);
|
|
||||||
board["C7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["C7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["C7"].IsPromoted.Should().Be(false);
|
|
||||||
board["D7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["D7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["D7"].IsPromoted.Should().Be(false);
|
|
||||||
board["E7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["E7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["E7"].IsPromoted.Should().Be(false);
|
|
||||||
board["F7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["F7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["F7"].IsPromoted.Should().Be(false);
|
|
||||||
board["G7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["G7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["G7"].IsPromoted.Should().Be(false);
|
|
||||||
board["H7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["H7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["H7"].IsPromoted.Should().Be(false);
|
|
||||||
board["I7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
board["I7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["I7"].IsPromoted.Should().Be(false);
|
|
||||||
|
|
||||||
board["A8"].Should().BeNull();
|
|
||||||
board["B8"].WhichPiece.Should().Be(WhichPiece.Rook);
|
|
||||||
board["B8"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["B8"].IsPromoted.Should().Be(false);
|
|
||||||
board["C8"].Should().BeNull();
|
|
||||||
board["D8"].Should().BeNull();
|
|
||||||
board["E8"].Should().BeNull();
|
|
||||||
board["F8"].Should().BeNull();
|
|
||||||
board["G8"].Should().BeNull();
|
|
||||||
board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop);
|
|
||||||
board["H8"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["H8"].IsPromoted.Should().Be(false);
|
|
||||||
board["I8"].Should().BeNull();
|
|
||||||
|
|
||||||
board["A9"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
board["A9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["A9"].IsPromoted.Should().Be(false);
|
|
||||||
board["B9"].WhichPiece.Should().Be(WhichPiece.Knight);
|
|
||||||
board["B9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["B9"].IsPromoted.Should().Be(false);
|
|
||||||
board["C9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
|
|
||||||
board["C9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["C9"].IsPromoted.Should().Be(false);
|
|
||||||
board["D9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
|
|
||||||
board["D9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["D9"].IsPromoted.Should().Be(false);
|
|
||||||
board["E9"].WhichPiece.Should().Be(WhichPiece.King);
|
|
||||||
board["E9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["E9"].IsPromoted.Should().Be(false);
|
|
||||||
board["F9"].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
|
|
||||||
board["F9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["F9"].IsPromoted.Should().Be(false);
|
|
||||||
board["G9"].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
|
|
||||||
board["G9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["G9"].IsPromoted.Should().Be(false);
|
|
||||||
board["H9"].WhichPiece.Should().Be(WhichPiece.Knight);
|
|
||||||
board["H9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["H9"].IsPromoted.Should().Be(false);
|
|
||||||
board["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
board["I9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
board["I9"].IsPromoted.Should().Be(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InitializeBoardStateWithMoves()
|
|
||||||
{
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
// P1 Pawn
|
|
||||||
new Move("A3", "A4")
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
shogi.Board["A3"].Should().BeNull();
|
|
||||||
shogi.Board["A4"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AllowValidMoves_AfterCheck()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
// P1 Pawn
|
|
||||||
new Move("C3", "C4"),
|
|
||||||
// P2 Pawn
|
|
||||||
new Move("G7", "G6"),
|
|
||||||
// P1 Bishop puts P2 in check
|
|
||||||
new Move("B2", "G7"),
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
shogi.InCheck.Should().Be(WhichPerspective.Player2);
|
|
||||||
|
|
||||||
// Act - P2 is able to un-check theirself.
|
|
||||||
/// P2 King moves out of check
|
|
||||||
var moveSuccess = shogi.Move(new Move("E9", "E8"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using var _ = new AssertionScope();
|
|
||||||
moveSuccess.Should().BeTrue();
|
|
||||||
shogi.InCheck.Should().BeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_MoveFromEmptyPosition()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var shogi = new ShogiModel();
|
|
||||||
shogi.Board["D5"].Should().BeNull();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var moveSuccess = shogi.Move(new Move("D5", "D6"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
moveSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["D5"].Should().BeNull();
|
|
||||||
shogi.Board["D6"].Should().BeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_MoveToCurrentPosition()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var shogi = new ShogiModel();
|
|
||||||
|
|
||||||
// Act - P1 "moves" pawn to the position it already exists at.
|
|
||||||
var moveSuccess = shogi.Move(new Move("A3", "A3"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
moveSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
shogi.Player1Hand.Should().BeEmpty();
|
|
||||||
shogi.Player2Hand.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_MoveSet()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var shogi = new ShogiModel();
|
|
||||||
|
|
||||||
// Act - Move Lance illegally
|
|
||||||
var moveSuccess = shogi.Move(new Move("A1", "D5"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
moveSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
shogi.Board["A5"].Should().BeNull();
|
|
||||||
shogi.Player1Hand.Should().BeEmpty();
|
|
||||||
shogi.Player2Hand.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_Ownership()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var shogi = new ShogiModel();
|
|
||||||
shogi.WhoseTurn.Should().Be(WhichPerspective.Player1);
|
|
||||||
shogi.Board["A7"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
|
|
||||||
// Act - Move Player2 Pawn when it is Player1 turn.
|
|
||||||
var moveSuccess = shogi.Move(new Move("A7", "A6"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
moveSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["A7"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
shogi.Board["A6"].Should().BeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_MoveThroughAllies()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var shogi = new ShogiModel();
|
|
||||||
|
|
||||||
// Act - Move P1 Lance through P1 Pawn.
|
|
||||||
var moveSuccess = shogi.Move(new Move("A1", "A5"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
moveSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["A1"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
shogi.Board["A3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
shogi.Board["A5"].Should().BeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_CaptureAlly()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var shogi = new ShogiModel();
|
|
||||||
|
|
||||||
// Act - P1 Knight tries to capture P1 Pawn.
|
|
||||||
var moveSuccess = shogi.Move(new Move("B1", "C3"));
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
moveSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["B1"].WhichPiece.Should().Be(WhichPiece.Knight);
|
|
||||||
shogi.Board["C3"].WhichPiece.Should().Be(WhichPiece.Pawn);
|
|
||||||
shogi.Player1Hand.Should().BeEmpty();
|
|
||||||
shogi.Player2Hand.Should().BeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidMoves_Check()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
// P1 Pawn
|
|
||||||
new Move("C3", "C4"),
|
|
||||||
// P2 Pawn
|
|
||||||
new Move("G7", "G6"),
|
|
||||||
// P1 Bishop puts P2 in check
|
|
||||||
new Move("B2", "G7")
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
shogi.InCheck.Should().Be(WhichPerspective.Player2);
|
|
||||||
|
|
||||||
// Act - P2 moves Lance while in check.
|
|
||||||
var moveSuccess = shogi.Move(new Move("I9", "I8"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
moveSuccess.Should().BeFalse();
|
|
||||||
shogi.InCheck.Should().Be(WhichPerspective.Player2);
|
|
||||||
shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
shogi.Board["I8"].Should().BeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidDrops_MoveSet()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
// P1 Pawn
|
|
||||||
new Move("C3", "C4"),
|
|
||||||
// P2 Pawn
|
|
||||||
new Move("I7", "I6"),
|
|
||||||
// P1 Bishop takes P2 Pawn.
|
|
||||||
new Move("B2", "G7"),
|
|
||||||
// P2 Gold, block check from P1 Bishop.
|
|
||||||
new Move("F9", "F8"),
|
|
||||||
// P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance
|
|
||||||
new Move("G7", "H8", true),
|
|
||||||
// P2 Pawn again
|
|
||||||
new Move("I6", "I5"),
|
|
||||||
// P1 Bishop takes P2 Knight
|
|
||||||
new Move("H8", "H9"),
|
|
||||||
// P2 Pawn again
|
|
||||||
new Move("I5", "I4"),
|
|
||||||
// P1 Bishop takes P2 Lance
|
|
||||||
new Move("H9", "I9"),
|
|
||||||
// P2 Pawn captures P1 Pawn
|
|
||||||
new Move("I4", "I3")
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
shogi.Player1Hand.Count.Should().Be(4);
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
|
|
||||||
shogi.WhoseTurn.Should().Be(WhichPerspective.Player1);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally placing Knight from the hand in farthest row.
|
|
||||||
shogi.Board["H9"].Should().BeNull();
|
|
||||||
var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9"));
|
|
||||||
dropSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["H9"].Should().BeNull();
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally placing Knight from the hand in second farthest row.
|
|
||||||
shogi.Board["H8"].Should().BeNull();
|
|
||||||
dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H8"));
|
|
||||||
dropSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["H8"].Should().BeNull();
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally place Lance from the hand.
|
|
||||||
shogi.Board["H9"].Should().BeNull();
|
|
||||||
dropSuccess = shogi.Move(new Move(WhichPiece.Knight, "H9"));
|
|
||||||
dropSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["H9"].Should().BeNull();
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally place Pawn from the hand.
|
|
||||||
shogi.Board["H9"].Should().BeNull();
|
|
||||||
dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, "H9"));
|
|
||||||
dropSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["H9"].Should().BeNull();
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
|
|
||||||
|
|
||||||
// Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn.
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void PreventInvalidDrop_Check()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
// P1 Pawn
|
|
||||||
new Move("C3", "C4"),
|
|
||||||
// P2 Pawn
|
|
||||||
new Move("G7", "G6"),
|
|
||||||
// P1 Pawn, arbitrary move.
|
|
||||||
new Move("A3", "A4"),
|
|
||||||
// P2 Bishop takes P1 Bishop
|
|
||||||
new Move("H8", "B2"),
|
|
||||||
// P1 Silver takes P2 Bishop
|
|
||||||
new Move("C1", "B2"),
|
|
||||||
// P2 Pawn, arbtrary move
|
|
||||||
new Move("A7", "A6"),
|
|
||||||
// P1 drop Bishop, place P2 in check
|
|
||||||
new Move(WhichPiece.Bishop, "G7")
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
shogi.InCheck.Should().Be(WhichPerspective.Player2);
|
|
||||||
shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
|
|
||||||
shogi.Board["E5"].Should().BeNull();
|
|
||||||
|
|
||||||
// Act - P2 places a Bishop while in check.
|
|
||||||
var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
dropSuccess.Should().BeFalse();
|
|
||||||
shogi.Board["E5"].Should().BeNull();
|
|
||||||
shogi.InCheck.Should().Be(WhichPerspective.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 ShogiModel(moves);
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
|
|
||||||
shogi.Board["I9"].Should().NotBeNull();
|
|
||||||
shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
shogi.Board["I9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Act - P1 tries to place a piece where an opponent's piece resides.
|
|
||||||
var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
dropSuccess.Should().BeFalse();
|
|
||||||
shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
|
|
||||||
shogi.Board["I9"].Should().NotBeNull();
|
|
||||||
shogi.Board["I9"].WhichPiece.Should().Be(WhichPiece.Lance);
|
|
||||||
shogi.Board["I9"].Owner.Should().Be(WhichPerspective.Player2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Check()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
// P1 Pawn
|
|
||||||
new Move("C3", "C4"),
|
|
||||||
// P2 Pawn
|
|
||||||
new Move("G7", "G6"),
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
|
|
||||||
// Act - P1 Bishop, check
|
|
||||||
shogi.Move(new Move("B2", "G7"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
shogi.InCheck.Should().Be(WhichPerspective.Player2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Promote()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
// P1 Pawn
|
|
||||||
new Move("C3", "C4" ),
|
|
||||||
// P2 Pawn
|
|
||||||
new Move("G7", "G6" )
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
|
|
||||||
// Act - P1 moves across promote threshold.
|
|
||||||
var moveSuccess = shogi.Move(new Move("B2", "G7", true));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
using (new AssertionScope())
|
|
||||||
{
|
|
||||||
moveSuccess.Should().BeTrue();
|
|
||||||
shogi.Board["B2"].Should().BeNull();
|
|
||||||
shogi.Board["G7"].Should().NotBeNull();
|
|
||||||
shogi.Board["G7"].WhichPiece.Should().Be(WhichPiece.Bishop);
|
|
||||||
shogi.Board["G7"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
shogi.Board["G7"].IsPromoted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CheckMate()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
// P1 Rook
|
|
||||||
new Move("H2", "E2"),
|
|
||||||
// P2 Gold
|
|
||||||
new Move("F9", "G8"),
|
|
||||||
// P1 Pawn
|
|
||||||
new Move("E3", "E4"),
|
|
||||||
// P2 other Gold
|
|
||||||
new Move("D9", "C8"),
|
|
||||||
// P1 same Pawn
|
|
||||||
new Move("E4", "E5"),
|
|
||||||
// P2 Pawn
|
|
||||||
new Move("E7", "E6"),
|
|
||||||
// P1 Pawn takes P2 Pawn
|
|
||||||
new Move("E5", "E6"),
|
|
||||||
// P2 King
|
|
||||||
new Move("E9", "E8"),
|
|
||||||
// P1 Pawn promotes, threatens P2 King
|
|
||||||
new Move("E6", "E7", true),
|
|
||||||
// P2 King retreat
|
|
||||||
new Move("E8", "E9"),
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
output.WriteLine(shogi.PrintStateAsAscii());
|
|
||||||
|
|
||||||
// Act - P1 Pawn wins by checkmate.
|
|
||||||
var moveSuccess = shogi.Move(new Move("E7", "E8"));
|
|
||||||
output.WriteLine(shogi.PrintStateAsAscii());
|
|
||||||
|
|
||||||
// Assert - checkmate
|
|
||||||
moveSuccess.Should().BeTrue();
|
|
||||||
shogi.IsCheckmate.Should().BeTrue();
|
|
||||||
shogi.InCheck.Should().Be(WhichPerspective.Player2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Capture()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var moves = new[]
|
|
||||||
{
|
|
||||||
new Move("C3", "C4"),
|
|
||||||
new Move("G7", "G6")
|
|
||||||
};
|
|
||||||
var shogi = new ShogiModel(moves);
|
|
||||||
|
|
||||||
// Act - P1 Bishop captures P2 Bishop
|
|
||||||
var moveSuccess = shogi.Move(new Move("B2", "H8"));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
moveSuccess.Should().BeTrue();
|
|
||||||
shogi.Board["B2"].Should().BeNull();
|
|
||||||
shogi.Board["H8"].WhichPiece.Should().Be(WhichPiece.Bishop);
|
|
||||||
shogi.Board["H8"].Owner.Should().Be(WhichPerspective.Player1);
|
|
||||||
shogi.Board.Values
|
|
||||||
.Where(p => p != null)
|
|
||||||
.Should().ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop);
|
|
||||||
|
|
||||||
shogi.Player1Hand
|
|
||||||
.Should()
|
|
||||||
.ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPerspective.Player1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
Shogi.Domain.UnitTests/NotationShould.cs
Normal file
27
Shogi.Domain.UnitTests/NotationShould.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using System.Numerics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Shogi.Domain.UnitTests
|
||||||
|
{
|
||||||
|
public class NotationShould
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ConvertFromNotationToVector()
|
||||||
|
{
|
||||||
|
Notation.FromBoardNotation("A1").Should().Be(new Vector2(0, 0));
|
||||||
|
Notation.FromBoardNotation("E5").Should().Be(new Vector2(4, 4));
|
||||||
|
Notation.FromBoardNotation("I9").Should().Be(new Vector2(8, 8));
|
||||||
|
Notation.FromBoardNotation("C3").Should().Be(new Vector2(2, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConvertFromVectorToNotation()
|
||||||
|
{
|
||||||
|
Notation.ToBoardNotation(new Vector2(0, 0)).Should().Be("A1");
|
||||||
|
Notation.ToBoardNotation(new Vector2(4, 4)).Should().Be("E5");
|
||||||
|
Notation.ToBoardNotation(new Vector2(8, 8)).Should().Be("I9");
|
||||||
|
Notation.ToBoardNotation(new Vector2(2, 2)).Should().Be("C3");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void InitializeBoardState()
|
public void InitializeBoardState()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
board["A1"]?.WhichPiece.Should().Be(WhichPiece.Lance);
|
board["A1"]?.WhichPiece.Should().Be(WhichPiece.Lance);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void MoveAPieceToAnEmptyPosition()
|
public void MoveAPieceToAnEmptyPosition()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
board["A4"].Should().BeNull();
|
board["A4"].Should().BeNull();
|
||||||
var expectedPiece = board["A3"];
|
var expectedPiece = board["A3"];
|
||||||
expectedPiece.Should().NotBeNull();
|
expectedPiece.Should().NotBeNull();
|
||||||
@@ -36,8 +36,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void AllowValidMoves_AfterCheck()
|
public void AllowValidMoves_AfterCheck()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
// P1 Pawn
|
// P1 Pawn
|
||||||
shogi.Move("C3", "C4", false);
|
shogi.Move("C3", "C4", false);
|
||||||
// P2 Pawn
|
// P2 Pawn
|
||||||
@@ -61,8 +61,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_MoveFromEmptyPosition()
|
public void PreventInvalidMoves_MoveFromEmptyPosition()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
board["D5"].Should().BeNull();
|
board["D5"].Should().BeNull();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -80,8 +80,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_MoveToCurrentPosition()
|
public void PreventInvalidMoves_MoveToCurrentPosition()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
var expectedPiece = board["A3"];
|
var expectedPiece = board["A3"];
|
||||||
|
|
||||||
// Act - P1 "moves" pawn to the position it already exists at.
|
// Act - P1 "moves" pawn to the position it already exists at.
|
||||||
@@ -101,8 +101,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_MoveSet()
|
public void PreventInvalidMoves_MoveSet()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
var expectedPiece = board["A1"];
|
var expectedPiece = board["A1"];
|
||||||
expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance);
|
expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance);
|
||||||
|
|
||||||
@@ -124,8 +124,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_Ownership()
|
public void PreventInvalidMoves_Ownership()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
var expectedPiece = board["A7"];
|
var expectedPiece = board["A7"];
|
||||||
expectedPiece!.Owner.Should().Be(WhichPlayer.Player2);
|
expectedPiece!.Owner.Should().Be(WhichPlayer.Player2);
|
||||||
board.WhoseTurn.Should().Be(WhichPlayer.Player1);
|
board.WhoseTurn.Should().Be(WhichPlayer.Player1);
|
||||||
@@ -146,8 +146,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_MoveThroughAllies()
|
public void PreventInvalidMoves_MoveThroughAllies()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
var lance = board["A1"];
|
var lance = board["A1"];
|
||||||
var pawn = board["A3"];
|
var pawn = board["A3"];
|
||||||
lance!.Owner.Should().Be(pawn!.Owner);
|
lance!.Owner.Should().Be(pawn!.Owner);
|
||||||
@@ -169,8 +169,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_CaptureAlly()
|
public void PreventInvalidMoves_CaptureAlly()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
var knight = board["B1"];
|
var knight = board["B1"];
|
||||||
var pawn = board["C3"];
|
var pawn = board["C3"];
|
||||||
knight!.Owner.Should().Be(pawn!.Owner);
|
knight!.Owner.Should().Be(pawn!.Owner);
|
||||||
@@ -193,8 +193,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidMoves_Check()
|
public void PreventInvalidMoves_Check()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
// P1 Pawn
|
// P1 Pawn
|
||||||
shogi.Move("C3", "C4", false);
|
shogi.Move("C3", "C4", false);
|
||||||
// P2 Pawn
|
// P2 Pawn
|
||||||
@@ -222,8 +222,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void PreventInvalidDrops_MoveSet()
|
public void PreventInvalidDrops_MoveSet()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var board = new ShogiBoardState();
|
var board = new BoardState();
|
||||||
var shogi = new Shogi(board);
|
var shogi = new Session(board);
|
||||||
// P1 Pawn
|
// P1 Pawn
|
||||||
shogi.Move("C3", "C4", false);
|
shogi.Move("C3", "C4", false);
|
||||||
// P2 Pawn
|
// P2 Pawn
|
||||||
@@ -361,8 +361,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void Check()
|
public void Check()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var boardState = new ShogiBoardState();
|
var boardState = new BoardState();
|
||||||
var shogi = new Shogi(boardState);
|
var shogi = new Session(boardState);
|
||||||
// P1 Pawn
|
// P1 Pawn
|
||||||
shogi.Move("C3", "C4", false);
|
shogi.Move("C3", "C4", false);
|
||||||
// P2 Pawn
|
// P2 Pawn
|
||||||
@@ -379,8 +379,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void Promote()
|
public void Promote()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var boardState = new ShogiBoardState();
|
var boardState = new BoardState();
|
||||||
var shogi = new Shogi(boardState);
|
var shogi = new Session(boardState);
|
||||||
// P1 Pawn
|
// P1 Pawn
|
||||||
shogi.Move("C3", "C4", false);
|
shogi.Move("C3", "C4", false);
|
||||||
// P2 Pawn
|
// P2 Pawn
|
||||||
@@ -404,8 +404,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void Capture()
|
public void Capture()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var boardState = new ShogiBoardState();
|
var boardState = new BoardState();
|
||||||
var shogi = new Shogi(boardState);
|
var shogi = new Session(boardState);
|
||||||
var p1Bishop = boardState["B2"];
|
var p1Bishop = boardState["B2"];
|
||||||
p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop);
|
p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop);
|
||||||
shogi.Move("C3", "C4", false);
|
shogi.Move("C3", "C4", false);
|
||||||
@@ -428,8 +428,8 @@ namespace Shogi.Domain.UnitTests
|
|||||||
public void CheckMate()
|
public void CheckMate()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var boardState = new ShogiBoardState();
|
var boardState = new BoardState();
|
||||||
var shogi = new Shogi(boardState);
|
var shogi = new Session(boardState);
|
||||||
// P1 Rook
|
// P1 Rook
|
||||||
shogi.Move("H2", "E2", false);
|
shogi.Move("H2", "E2", false);
|
||||||
// P2 Gold
|
// P2 Gold
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
using Shogi.Domain.Pieces;
|
using Shogi.Domain.Pieces;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.Pieces.Piece>;
|
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.Pieces.Piece>;
|
||||||
|
|
||||||
namespace Shogi.Domain
|
namespace Shogi.Domain
|
||||||
{
|
{
|
||||||
// TODO: Avoid extending dictionary. Use composition instead.
|
public class BoardState
|
||||||
// Then validation can occur when assigning a piece to a position.
|
|
||||||
public class ShogiBoardState
|
|
||||||
{
|
{
|
||||||
private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
|
|
||||||
private static readonly char A = 'A';
|
|
||||||
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key is position notation, such as "E4".
|
/// Key is position notation, such as "E4".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Dictionary<string, Piece?> board;
|
private readonly Dictionary<string, Piece?> board;
|
||||||
|
|
||||||
public ShogiBoardState()
|
|
||||||
|
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);
|
board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
||||||
InitializeBoardState();
|
InitializeBoardState();
|
||||||
@@ -25,13 +29,14 @@ namespace Shogi.Domain
|
|||||||
PreviousMoveTo = Vector2.Zero;
|
PreviousMoveTo = Vector2.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, Piece?> State => board;
|
||||||
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
public List<Piece> ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand;
|
||||||
public Vector2 Player1KingPosition => FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||||
{
|
{
|
||||||
var piece = kvp.Value;
|
var piece = kvp.Value;
|
||||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1;
|
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1;
|
||||||
}).Key);
|
}).Key);
|
||||||
public Vector2 Player2KingPosition => FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp =>
|
||||||
{
|
{
|
||||||
var piece = kvp.Value;
|
var piece = kvp.Value;
|
||||||
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2;
|
return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2;
|
||||||
@@ -47,12 +52,12 @@ namespace Shogi.Domain
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy constructor.
|
/// Copy constructor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ShogiBoardState(ShogiBoardState other) : this()
|
public BoardState(BoardState other) : this()
|
||||||
{
|
{
|
||||||
foreach (var kvp in other.board)
|
foreach (var kvp in other.board)
|
||||||
{
|
{
|
||||||
// Replace copy constructor with static factory method in Piece.cs
|
// Replace copy constructor with static factory method in Piece.cs
|
||||||
board[kvp.Key] = kvp.Value == null ? null : Piece.Create(kvp.Value);
|
board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value);
|
||||||
}
|
}
|
||||||
WhoseTurn = other.WhoseTurn;
|
WhoseTurn = other.WhoseTurn;
|
||||||
InCheck = other.InCheck;
|
InCheck = other.InCheck;
|
||||||
@@ -71,14 +76,14 @@ namespace Shogi.Domain
|
|||||||
|
|
||||||
public Piece? this[Vector2 vector]
|
public Piece? this[Vector2 vector]
|
||||||
{
|
{
|
||||||
get => this[ToBoardNotation(vector)];
|
get => this[Notation.ToBoardNotation(vector)];
|
||||||
set => this[ToBoardNotation(vector)] = value;
|
set => this[Notation.ToBoardNotation(vector)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Piece? this[int x, int y]
|
public Piece? this[int x, int y]
|
||||||
{
|
{
|
||||||
get => this[ToBoardNotation(x, y)];
|
get => this[Notation.ToBoardNotation(x, y)];
|
||||||
set => this[ToBoardNotation(x, y)] = value;
|
set => this[Notation.ToBoardNotation(x, y)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
|
internal void RememberAsMostRecentMove(Vector2 from, Vector2 to)
|
||||||
@@ -111,7 +116,7 @@ namespace Shogi.Domain
|
|||||||
|
|
||||||
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
internal List<BoardTile> GetTilesOccupiedBy(WhichPlayer whichPlayer) => board
|
||||||
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
.Where(kvp => kvp.Value?.Owner == whichPlayer)
|
||||||
.Select(kvp => new BoardTile(FromBoardNotation(kvp.Key), kvp.Value!))
|
.Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
internal void Capture(Vector2 to)
|
internal void Capture(Vector2 to)
|
||||||
@@ -144,27 +149,6 @@ namespace Shogi.Domain
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
public static string ToBoardNotation(Vector2 vector)
|
|
||||||
{
|
|
||||||
return ToBoardNotation((int)vector.X, (int)vector.Y);
|
|
||||||
}
|
|
||||||
private static string ToBoardNotation(int x, int y)
|
|
||||||
{
|
|
||||||
var file = (char)(x + A);
|
|
||||||
var rank = y + 1;
|
|
||||||
return $"{file}{rank}";
|
|
||||||
}
|
|
||||||
public static Vector2 FromBoardNotation(string notation)
|
|
||||||
{
|
|
||||||
if (Regex.IsMatch(notation, BoardNotationRegex))
|
|
||||||
{
|
|
||||||
var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase);
|
|
||||||
char file = match.Groups["file"].Value[0];
|
|
||||||
int rank = int.Parse(match.Groups["rank"].Value);
|
|
||||||
return new Vector2(file - A, rank - 1);
|
|
||||||
}
|
|
||||||
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeBoardState()
|
private void InitializeBoardState()
|
||||||
{
|
{
|
||||||
33
Shogi.Domain/Notation.cs
Normal file
33
Shogi.Domain/Notation.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Shogi.Domain
|
||||||
|
{
|
||||||
|
public static class Notation
|
||||||
|
{
|
||||||
|
private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
|
||||||
|
private static readonly char A = 'A';
|
||||||
|
|
||||||
|
public static string ToBoardNotation(Vector2 vector)
|
||||||
|
{
|
||||||
|
return ToBoardNotation((int)vector.X, (int)vector.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToBoardNotation(int x, int y)
|
||||||
|
{
|
||||||
|
var file = (char)(x + A);
|
||||||
|
var rank = y + 1;
|
||||||
|
return $"{file}{rank}";
|
||||||
|
}
|
||||||
|
public static Vector2 FromBoardNotation(string notation)
|
||||||
|
{
|
||||||
|
if (Regex.IsMatch(notation, BoardNotationRegex))
|
||||||
|
{
|
||||||
|
var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase);
|
||||||
|
char file = match.Groups["file"].Value[0];
|
||||||
|
int rank = int.Parse(match.Groups["rank"].Value);
|
||||||
|
return new Vector2(file - A, rank - 1);
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ namespace Shogi.Domain.Pieces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a clone of an existing piece.
|
/// Creates a clone of an existing piece.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Piece Create(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
|
public static Piece CreateCopy(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
|
||||||
public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
|
||||||
{
|
{
|
||||||
return piece switch
|
return piece switch
|
||||||
@@ -25,8 +25,6 @@ namespace Shogi.Domain.Pieces
|
|||||||
_ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
|
_ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: MoveSet doesn't account for Player2's pieces which are upside-down.
|
|
||||||
public abstract IEnumerable<Path> MoveSet { get; }
|
public abstract IEnumerable<Path> MoveSet { get; }
|
||||||
public WhichPiece WhichPiece { get; }
|
public WhichPiece WhichPiece { get; }
|
||||||
public WhichPlayer Owner { get; private set; }
|
public WhichPlayer Owner { get; private set; }
|
||||||
|
|||||||
@@ -8,21 +8,38 @@ namespace Shogi.Domain
|
|||||||
/// The board is always from Player1's perspective.
|
/// The board is always from Player1's perspective.
|
||||||
/// [0,0] is the lower-left position, [8,8] is the higher-right position
|
/// [0,0] is the lower-left position, [8,8] is the higher-right position
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Shogi
|
public sealed class Session
|
||||||
{
|
{
|
||||||
private readonly ShogiBoardState boardState;
|
private readonly Tuple<string, string> Players;
|
||||||
|
private readonly BoardState boardState;
|
||||||
private readonly StandardRules rules;
|
private readonly StandardRules rules;
|
||||||
|
private readonly SessionMetadata metadata;
|
||||||
|
|
||||||
public Shogi() : this(new ShogiBoardState())
|
public Session() : this(new BoardState())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Shogi(ShogiBoardState board)
|
public Session(BoardState state) : this(state, new SessionMetadata(string.Empty, false, string.Empty, string.Empty))
|
||||||
{
|
{
|
||||||
this.rules = new StandardRules(board);
|
|
||||||
this.boardState = board;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
/// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -33,7 +50,7 @@ namespace Shogi.Domain
|
|||||||
/// <exception cref="InvalidOperationException"></exception>
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
public void Move(string from, string to, bool isPromotion)
|
public void Move(string from, string to, bool isPromotion)
|
||||||
{
|
{
|
||||||
var simulationState = new ShogiBoardState(boardState);
|
var simulationState = new BoardState(boardState);
|
||||||
var simulation = new StandardRules(simulationState);
|
var simulation = new StandardRules(simulationState);
|
||||||
var moveResult = simulation.Move(from, to, isPromotion);
|
var moveResult = simulation.Move(from, to, isPromotion);
|
||||||
if (!moveResult.Success)
|
if (!moveResult.Success)
|
||||||
@@ -83,7 +100,7 @@ namespace Shogi.Domain
|
|||||||
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var toVector = ShogiBoardState.FromBoardNotation(to);
|
var toVector = Notation.FromBoardNotation(to);
|
||||||
switch (pieceInHand)
|
switch (pieceInHand)
|
||||||
{
|
{
|
||||||
case WhichPiece.Knight:
|
case WhichPiece.Knight:
|
||||||
@@ -109,7 +126,7 @@ namespace Shogi.Domain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempBoard = new ShogiBoardState(boardState);
|
var tempBoard = new BoardState(boardState);
|
||||||
var simulation = new StandardRules(tempBoard);
|
var simulation = new StandardRules(tempBoard);
|
||||||
var moveResult = simulation.Move(pieceInHand, to);
|
var moveResult = simulation.Move(pieceInHand, to);
|
||||||
if (!moveResult.Success)
|
if (!moveResult.Success)
|
||||||
26
Shogi.Domain/SessionMetadata.cs
Normal file
26
Shogi.Domain/SessionMetadata.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ namespace Shogi.Domain
|
|||||||
{
|
{
|
||||||
internal class StandardRules
|
internal class StandardRules
|
||||||
{
|
{
|
||||||
private readonly ShogiBoardState boardState;
|
private readonly BoardState boardState;
|
||||||
|
|
||||||
internal StandardRules(ShogiBoardState board)
|
internal StandardRules(BoardState board)
|
||||||
{
|
{
|
||||||
boardState = board;
|
boardState = board;
|
||||||
}
|
}
|
||||||
@@ -22,8 +22,8 @@ namespace Shogi.Domain
|
|||||||
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the move.</returns>
|
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the move.</returns>
|
||||||
internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false)
|
internal MoveResult Move(string fromNotation, string toNotation, bool isPromotionRequested = false)
|
||||||
{
|
{
|
||||||
var from = ShogiBoardState.FromBoardNotation(fromNotation);
|
var from = Notation.FromBoardNotation(fromNotation);
|
||||||
var to = ShogiBoardState.FromBoardNotation(toNotation);
|
var to = Notation.FromBoardNotation(toNotation);
|
||||||
var fromPiece = boardState[from];
|
var fromPiece = boardState[from];
|
||||||
if (fromPiece == null)
|
if (fromPiece == null)
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ namespace Shogi.Domain
|
|||||||
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns>
|
/// <returns>A <see cref="MoveResult" /> describing the success or failure of the simulation.</returns>
|
||||||
internal MoveResult Move(WhichPiece pieceInHand, string toNotation)
|
internal MoveResult Move(WhichPiece pieceInHand, string toNotation)
|
||||||
{
|
{
|
||||||
var to = ShogiBoardState.FromBoardNotation(toNotation);
|
var to = Notation.FromBoardNotation(toNotation);
|
||||||
var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
|
var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand);
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
{
|
{
|
||||||
@@ -130,7 +130,7 @@ namespace Shogi.Domain
|
|||||||
// Get line equation from king through the now-unoccupied location.
|
// Get line equation from king through the now-unoccupied location.
|
||||||
var direction = Vector2.Subtract(kingPosition, boardState.PreviousMoveFrom);
|
var direction = Vector2.Subtract(kingPosition, boardState.PreviousMoveFrom);
|
||||||
var slope = Math.Abs(direction.Y / direction.X);
|
var slope = Math.Abs(direction.Y / direction.X);
|
||||||
var path = ShogiBoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, Vector2.Normalize(direction));
|
var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMoveFrom, Vector2.Normalize(direction));
|
||||||
var threat = boardState.QueryFirstPieceInPath(path);
|
var threat = boardState.QueryFirstPieceInPath(path);
|
||||||
if (threat == null || threat.Owner == previousMovedPiece.Owner) return false;
|
if (threat == null || threat.Owner == previousMovedPiece.Owner) return false;
|
||||||
// If absolute slope is 45°, look for a bishop along the line.
|
// If absolute slope is 45°, look for a bishop along the line.
|
||||||
@@ -253,7 +253,7 @@ namespace Shogi.Domain
|
|||||||
var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget);
|
var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget);
|
||||||
return path
|
return path
|
||||||
.SkipLast(1)
|
.SkipLast(1)
|
||||||
.All(position => boardState[ShogiBoardState.ToBoardNotation(position)] == null);
|
.All(position => boardState[Notation.ToBoardNotation(position)] == null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user