Before changing Piece[,] to Dictionary<string,Piece>

This commit is contained in:
2021-07-26 06:28:56 -05:00
parent f8f779e84c
commit 178cb00253
73 changed files with 1537 additions and 1418 deletions

View File

@@ -10,8 +10,4 @@
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" /> <PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,7 +1,6 @@
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines; using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Running; using BenchmarkDotNet.Running;
using Gameboard.ShogiUI.Rules;
using System; using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@@ -10,7 +9,6 @@ namespace Benchmarking
{ {
public class Benchmarks public class Benchmarks
{ {
private readonly Move[] moves;
private readonly Vector2[] directions; private readonly Vector2[] directions;
private readonly Consumer consumer = new(); private readonly Consumer consumer = new();
@@ -48,21 +46,11 @@ namespace Benchmarking
//[Benchmark] //[Benchmark]
public void One() public void One()
{ {
var board = new ShogiBoard();
foreach (var move in moves)
{
board.Move(move);
}
} }
//[Benchmark] //[Benchmark]
public void Two() public void Two()
{ {
var board = new ShogiBoard();
foreach (var move in moves)
{
//board.TryMove2(move);
}
} }

View File

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

View File

@@ -1,27 +0,0 @@
using System.Diagnostics;
using System.Numerics;
namespace Gameboard.ShogiUI.Rules
{
[DebuggerDisplay("{From} - {To}")]
public class Move
{
public WhichPiece? PieceFromHand { get; }
public Vector2? From { get; }
public Vector2 To { get; }
public bool IsPromotion { get; }
public Move(Vector2 from, Vector2 to, bool isPromotion)
{
From = from;
To = to;
IsPromotion = isPromotion;
}
public Move(WhichPiece pieceFromHand, Vector2 to)
{
PieceFromHand = pieceFromHand;
To = to;
}
}
}

View File

@@ -1,39 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules.Pieces
{
public class Bishop : Piece
{
private static readonly List<PathFinding.Move> Moves = 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)
};
private static readonly List<PathFinding.Move> PromotedMoves = 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 Bishop(WhichPlayer owner) : base(WhichPiece.Bishop, owner)
{
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, PromotedMoves);
}
public override Piece DeepClone()
{
var clone = new Bishop(Owner);
if (IsPromoted) clone.Promote();
return clone;
}
}
}

View File

@@ -1,30 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules.Pieces
{
public class GoldenGeneral : Piece
{
public static readonly List<PathFinding.Move> Moves = 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 GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldGeneral, owner)
{
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, Moves);
}
public override Piece DeepClone()
{
var clone = new GoldenGeneral(Owner);
if (IsPromoted) clone.Promote();
return clone;
}
}
}

View File

@@ -1,32 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules.Pieces
{
public class King : Piece
{
private static readonly List<PathFinding.Move> Moves = 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 King(WhichPlayer owner) : base(WhichPiece.King, owner)
{
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, Moves);
}
public override Piece DeepClone()
{
var clone = new King(Owner);
if (IsPromoted) clone.Promote();
return clone;
}
}
}

View File

@@ -1,27 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules.Pieces
{
public class Knight : Piece
{
private static readonly List<PathFinding.Move> Moves = new(2)
{
new PathFinding.Move(Direction.KnightLeft),
new PathFinding.Move(Direction.KnightRight)
};
public Knight(WhichPlayer owner) : base(WhichPiece.Knight, owner)
{
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves);
}
public override Piece DeepClone()
{
var clone = new Knight(Owner);
if (IsPromoted) clone.Promote();
return clone;
}
}
}

View File

@@ -1,26 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules.Pieces
{
public class Lance : Piece
{
private static readonly List<PathFinding.Move> Moves = new(1)
{
new PathFinding.Move(Direction.Up, Distance.MultiStep),
};
public Lance(WhichPlayer owner) : base(WhichPiece.Lance, owner)
{
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves);
}
public override Piece DeepClone()
{
var clone = new Lance(Owner);
if (IsPromoted) clone.Promote();
return clone;
}
}
}

View File

@@ -1,26 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules.Pieces
{
public class Pawn : Piece
{
private static readonly List<PathFinding.Move> Moves = new(1)
{
new PathFinding.Move(Direction.Up)
};
public Pawn(WhichPlayer owner) : base(WhichPiece.Pawn, owner)
{
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves);
}
public override Piece DeepClone()
{
var clone = new Pawn(Owner);
if (IsPromoted) clone.Promote();
return clone;
}
}
}

View File

@@ -1,47 +0,0 @@
using PathFinding;
using System.Diagnostics;
namespace Gameboard.ShogiUI.Rules.Pieces
{
[DebuggerDisplay("{WhichPiece} {Owner}")]
public abstract class Piece : IPlanarElement
{
protected MoveSet promotedMoveSet;
protected MoveSet moveSet;
public MoveSet MoveSet => IsPromoted ? promotedMoveSet : moveSet;
public abstract Piece DeepClone();
public WhichPiece WhichPiece { get; }
public WhichPlayer Owner { get; private set; }
public bool IsPromoted { get; private set; }
public bool IsUpsideDown => Owner == WhichPlayer.Player2;
public Piece(WhichPiece piece, WhichPlayer owner)
{
WhichPiece = piece;
Owner = owner;
IsPromoted = false;
}
public bool CanPromote => !IsPromoted
&& WhichPiece != WhichPiece.King
&& WhichPiece != WhichPiece.GoldGeneral;
public void ToggleOwnership()
{
Owner = Owner == WhichPlayer.Player1
? WhichPlayer.Player2
: WhichPlayer.Player1;
}
public void Promote() => IsPromoted = CanPromote;
public void Demote() => IsPromoted = false;
public void Capture()
{
ToggleOwnership();
Demote();
}
}
}

View File

@@ -1,39 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules.Pieces
{
public class Rook : Piece
{
private static readonly List<PathFinding.Move> Moves = 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)
};
private static readonly List<PathFinding.Move> PromotedMoves = 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 Rook(WhichPlayer owner) : base(WhichPiece.Rook, owner)
{
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, PromotedMoves);
}
public override Piece DeepClone()
{
var clone = new Rook(Owner);
if (IsPromoted) clone.Promote();
return clone;
}
}
}

View File

@@ -1,29 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules.Pieces
{
public class SilverGeneral : Piece
{
private static readonly List<PathFinding.Move> Moves = 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)
};
public SilverGeneral(WhichPlayer owner) : base(WhichPiece.SilverGeneral, owner)
{
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves);
}
public override Piece DeepClone()
{
var clone = new SilverGeneral(Owner);
if (IsPromoted) clone.Promote();
return clone;
}
}
}

View File

@@ -1,14 +0,0 @@
namespace Gameboard.ShogiUI.Rules
{
public enum WhichPiece
{
King,
GoldGeneral,
SilverGeneral,
Bishop,
Rook,
Knight,
Lance,
Pawn
}
}

View File

@@ -1,8 +0,0 @@
namespace Gameboard.ShogiUI.Rules
{
public enum WhichPlayer
{
Player1,
Player2
}
}

View File

@@ -6,13 +6,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public class JoinByCodeRequest : IRequest public class JoinByCodeRequest : IRequest
{ {
public ClientAction Action { get; set; } public ClientAction Action { get; set; }
public string JoinCode { get; set; } public string JoinCode { get; set; } = "";
} }
public class JoinGameRequest : IRequest public class JoinGameRequest : IRequest
{ {
public ClientAction Action { get; set; } public ClientAction Action { get; set; }
public string GameName { get; set; } public string GameName { get; set; } = "";
} }
public class JoinGameResponse : IResponse public class JoinGameResponse : IResponse
@@ -25,6 +25,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public JoinGameResponse(ClientAction action) public JoinGameResponse(ClientAction action)
{ {
Action = action.ToString(); Action = action.ToString();
Error = "";
GameName = "";
PlayerName = "";
} }
} }
} }

View File

@@ -1,6 +1,7 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
{ {
@@ -13,11 +14,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
{ {
public string Action { get; } public string Action { get; }
public string Error { get; set; } public string Error { get; set; }
public ICollection<Game> Games { get; set; } public IReadOnlyList<Game> Games { get; set; }
public ListGamesResponse(ClientAction action) public ListGamesResponse(ClientAction action)
{ {
Action = action.ToString(); Action = action.ToString();
Error = "";
Games = new Collection<Game>();
} }
} }
} }

View File

@@ -1,19 +1,22 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
{ {
public class LoadGameRequest : IRequest public class LoadGameRequest : IRequest
{ {
public ClientAction Action { get; set; } public ClientAction Action { get; set; }
public string GameName { get; set; } public string GameName { get; set; } = "";
} }
public class LoadGameResponse : IResponse public class LoadGameResponse : IResponse
{ {
public string Action { get; } public string Action { get; }
public Game Game { get; set; } public Game Game { get; set; }
public WhichPlayer PlayerPerspective { get; set; }
public BoardState BoardState { get; set; } public BoardState BoardState { get; set; }
public IList<Move> MoveHistory { get; set; }
public string Error { get; set; } public string Error { get; set; }
public LoadGameResponse(ClientAction action) public LoadGameResponse(ClientAction action)

View File

@@ -8,5 +8,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
public Piece[,] Board { get; set; } = new Piece[0, 0]; public Piece[,] Board { get; set; } = new Piece[0, 0];
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 WhichPlayer? PlayerInCheck { get; set; }
public WhichPlayer WhoseTurn { get; set; }
} }
} }

View File

@@ -1,14 +1,33 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
{ {
public class Game public class Game
{ {
public string Player1 { get; set; } = string.Empty;
public string? Player2 { get; set; } = string.Empty;
public string GameName { get; set; } = string.Empty; public string GameName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Players[0] is the session owner, Players[1] is the other guy /// Players[0] is the session owner, Players[1] is the other person.
/// </summary> /// </summary>
public IReadOnlyList<string> Players { get; set; } = Array.Empty<string>(); public IReadOnlyList<string> Players
{
get
{
var list = new List<string>(2) { Player1 };
if (!string.IsNullOrEmpty(Player2)) list.Add(Player2);
return list;
}
}
public Game()
{
}
public Game(string gameName, string player1, string? player2 = null)
{
GameName = gameName;
Player1 = player1;
Player2 = player2;
}
} }
} }

View File

@@ -17,7 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CouchDB", "CouchDB\CouchDB.csproj", "{EDFED1DF-253D-463B-842A-0B66F95214A7}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CouchDB", "CouchDB\CouchDB.csproj", "{EDFED1DF-253D-463B-842A-0B66F95214A7}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj", "{D7130FAF-CEC4-4567-A9F0-22C060E9B508}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -49,16 +49,17 @@ Global
{EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.Build.0 = Release|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.Build.0 = Release|Any CPU
{D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.Build.0 = Debug|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.ActiveCfg = Release|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.Build.0 = Release|Any CPU {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
{12530716-C11E-40CE-9F71-CCCC243F03E1} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}

View File

@@ -1,6 +1,5 @@
using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -14,14 +13,14 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
[Route("[controller]")] [Route("[controller]")]
public class GameController : ControllerBase public class GameController : ControllerBase
{ {
private readonly IGameboardRepositoryManager manager; private readonly IGameboardManager manager;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketConnectionManager communicationManager;
private readonly IGameboardRepository repository; private readonly IGameboardRepository repository;
public GameController( public GameController(
IGameboardRepository repository, IGameboardRepository repository,
IGameboardRepositoryManager manager, IGameboardManager manager,
ISocketCommunicationManager communicationManager) ISocketConnectionManager communicationManager)
{ {
this.manager = manager; this.manager = manager;
this.communicationManager = communicationManager; this.communicationManager = communicationManager;

View File

@@ -1,6 +1,5 @@
using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -17,13 +16,13 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
{ {
private readonly ILogger<SocketController> logger; private readonly ILogger<SocketController> logger;
private readonly ISocketTokenManager tokenManager; private readonly ISocketTokenManager tokenManager;
private readonly IGameboardRepositoryManager gameboardManager; private readonly IGameboardManager gameboardManager;
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardRepository gameboardRepository;
public SocketController( public SocketController(
ILogger<SocketController> logger, ILogger<SocketController> logger,
ISocketTokenManager tokenManager, ISocketTokenManager tokenManager,
IGameboardRepositoryManager gameboardManager, IGameboardManager gameboardManager,
IGameboardRepository gameboardRepository) IGameboardRepository gameboardRepository)
{ {
this.logger = logger; this.logger = logger;

View File

@@ -1,14 +1,13 @@
using Gameboard.ShogiUI.Rules; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Rules.Pieces;
using System; using System;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Gameboard.ShogiUI.UnitTests.Rules namespace Gameboard.ShogiUI.Sockets.Extensions
{ {
public static class BoardStateExtensions public static class ModelExtensions
{ {
public static string GetShortName(this Piece self) public static string GetShortName(this Models.Piece self)
{ {
var name = self.WhichPiece switch var name = self.WhichPiece switch
{ {
@@ -27,7 +26,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
return name; return name;
} }
public static void PrintStateAsAscii(this ShogiBoard self) public static void PrintStateAsAscii(this Models.Shogi self)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append(" Player 2(.)"); builder.Append(" Player 2(.)");
@@ -41,7 +40,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
builder.Append('|'); builder.Append('|');
for (var x = 0; x < 9; x++) for (var x = 0; x < 9; x++)
{ {
var piece = self.Board[x, y]; var piece = self.Board[y, x];
if (piece == null) if (piece == null)
{ {
builder.Append(" "); builder.Append(" ");

View File

@@ -8,14 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="Repositories\CouchModels\Readme.md" /> <PackageReference Include="FluentValidation" Version="10.3.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="Repositories\CouchModels\Readme.md" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityModel" Version="5.0.0" /> <PackageReference Include="IdentityModel" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="5.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="5.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.2" />
@@ -26,8 +19,8 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CouchDB\CouchDB.csproj" /> <ProjectReference Include="..\CouchDB\CouchDB.csproj" />
<ProjectReference Include="..\Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj" />
<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" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,33 @@
using Gameboard.ShogiUI.Sockets.Models;
using System.Collections.Concurrent;
namespace Gameboard.ShogiUI.Sockets.Managers
{
public interface IActiveSessionManager
{
void Add(Session session);
Session? Get(string sessionName);
}
// TODO: Consider moving this class' functionality into the ConnectionManager class.
public class ActiveSessionManager : IActiveSessionManager
{
private readonly ConcurrentDictionary<string, Session> Sessions;
public ActiveSessionManager()
{
Sessions = new ConcurrentDictionary<string, Session>();
}
public void Add(Session session) => Sessions.TryAdd(session.Name, session);
public Session? Get(string sessionName)
{
if (Sessions.TryGetValue(sessionName, out var session))
{
return session;
}
return null;
}
}
}

View File

@@ -1,32 +0,0 @@
using Gameboard.ShogiUI.Rules;
using System.Collections.Concurrent;
namespace Gameboard.ShogiUI.Sockets.Managers
{
public interface IBoardManager
{
void Add(string sessionName, ShogiBoard board);
ShogiBoard? Get(string sessionName);
}
public class BoardManager : IBoardManager
{
private readonly ConcurrentDictionary<string, ShogiBoard> Boards;
public BoardManager()
{
Boards = new ConcurrentDictionary<string, ShogiBoard>();
}
public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board);
public ShogiBoard? Get(string sessionName)
{
if (Boards.TryGetValue(sessionName, out var board))
{
return board;
}
return null;
}
}
}

View File

@@ -1,5 +1,4 @@
using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,20 +13,20 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
// It can be an API route and still tell socket connections about the new session. // It can be an API route and still tell socket connections about the new session.
public class CreateGameHandler : ICreateGameHandler public class CreateGameHandler : ICreateGameHandler
{ {
private readonly IGameboardRepositoryManager manager; private readonly IGameboardManager manager;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketConnectionManager connectionManager;
public CreateGameHandler( public CreateGameHandler(
ISocketCommunicationManager communicationManager, ISocketConnectionManager communicationManager,
IGameboardRepositoryManager manager) IGameboardManager manager)
{ {
this.manager = manager; this.manager = manager;
this.communicationManager = communicationManager; this.connectionManager = communicationManager;
} }
public async Task Handle(CreateGameRequest request, string userName) public async Task Handle(CreateGameRequest request, string userName)
{ {
var model = new Session(request.GameName, request.IsPrivate, userName); var model = new SessionMetadata(request.GameName, request.IsPrivate, userName, null);
var success = await manager.CreateSession(model); var success = await manager.CreateSession(model);
if (!success) if (!success)
@@ -36,7 +35,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{ {
Error = "Unable to create game with this name." Error = "Unable to create game with this name."
}; };
await communicationManager.BroadcastToPlayers(error, userName); await connectionManager.BroadcastToPlayers(error, userName);
} }
var response = new CreateGameResponse(request.Action) var response = new CreateGameResponse(request.Action)
@@ -46,8 +45,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
}; };
var task = request.IsPrivate var task = request.IsPrivate
? communicationManager.BroadcastToPlayers(response, userName) ? connectionManager.BroadcastToPlayers(response, userName)
: communicationManager.BroadcastToAll(response); : connectionManager.BroadcastToAll(response);
await task; await task;
} }

View File

@@ -11,10 +11,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
public class JoinByCodeHandler : IJoinByCodeHandler public class JoinByCodeHandler : IJoinByCodeHandler
{ {
private readonly IGameboardRepository repository; private readonly IGameboardRepository repository;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketConnectionManager communicationManager;
public JoinByCodeHandler( public JoinByCodeHandler(
ISocketCommunicationManager communicationManager, ISocketConnectionManager communicationManager,
IGameboardRepository repository) IGameboardRepository repository)
{ {
this.repository = repository; this.repository = repository;

View File

@@ -1,5 +1,5 @@
using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
@@ -10,40 +10,34 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
} }
public class JoinGameHandler : IJoinGameHandler public class JoinGameHandler : IJoinGameHandler
{ {
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardManager gameboardManager;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketConnectionManager connectionManager;
public JoinGameHandler( public JoinGameHandler(
ISocketCommunicationManager communicationManager, ISocketConnectionManager communicationManager,
IGameboardRepository gameboardRepository) IGameboardManager gameboardManager)
{ {
this.gameboardRepository = gameboardRepository; this.gameboardManager = gameboardManager;
this.communicationManager = communicationManager; this.connectionManager = communicationManager;
} }
public async Task Handle(JoinGameRequest request, string userName) public async Task Handle(JoinGameRequest request, string userName)
{ {
//var request = JsonConvert.DeserializeObject<JoinGameRequest>(json); var joinSucceeded = await gameboardManager.AssignPlayer2ToSession(request.GameName, userName);
//var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession var response = new JoinGameResponse(ClientAction.JoinGame)
//{ {
// PlayerName = userName, PlayerName = userName,
// SessionName = request.GameName GameName = request.GameName
//}); };
if (joinSucceeded)
//var response = new JoinGameResponse(ClientAction.JoinGame) {
//{ await connectionManager.BroadcastToAll(response);
// PlayerName = userName, }
// GameName = request.GameName else
//}; {
//if (joinSucceeded) response.Error = "Game is full or does not exist.";
//{ await connectionManager.BroadcastToPlayers(response, userName);
// await communicationManager.BroadcastToAll(response); }
//}
//else
//{
// response.Error = "Game is full.";
// await communicationManager.BroadcastToPlayers(response, userName);
//}
} }
} }
} }

View File

@@ -11,15 +11,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
Task Handle(ListGamesRequest request, string userName); Task Handle(ListGamesRequest request, string userName);
} }
// TODO: This doesn't need to be a socket action.
// It can be an HTTP route.
public class ListGamesHandler : IListGamesHandler public class ListGamesHandler : IListGamesHandler
{ {
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketConnectionManager communicationManager;
private readonly IGameboardRepository repository; private readonly IGameboardRepository repository;
public ListGamesHandler( public ListGamesHandler(
ISocketCommunicationManager communicationManager, ISocketConnectionManager communicationManager,
IGameboardRepository repository) IGameboardRepository repository)
{ {
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
@@ -28,12 +26,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
public async Task Handle(ListGamesRequest _, string userName) public async Task Handle(ListGamesRequest _, string userName)
{ {
var sessions = await repository.ReadSessions(); var sessions = await repository.ReadSessionMetadatas();
var games = sessions.Select(s => s.ToServiceModel()); // yuck var games = sessions.Select(s => new Game(s.Name, s.Player1, s.Player2)).ToList();
var response = new ListGamesResponse(ClientAction.ListGames) var response = new ListGamesResponse(ClientAction.ListGames)
{ {
Games = games.ToList() Games = games
}; };
await communicationManager.BroadcastToPlayers(response, userName); await communicationManager.BroadcastToPlayers(response, userName);

View File

@@ -1,4 +1,4 @@
using Gameboard.ShogiUI.Rules; using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
@@ -20,14 +20,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{ {
private readonly ILogger<LoadGameHandler> logger; private readonly ILogger<LoadGameHandler> logger;
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardRepository gameboardRepository;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketConnectionManager communicationManager;
private readonly IBoardManager boardManager; private readonly IActiveSessionManager boardManager;
public LoadGameHandler( public LoadGameHandler(
ILogger<LoadGameHandler> logger, ILogger<LoadGameHandler> logger,
ISocketCommunicationManager communicationManager, ISocketConnectionManager communicationManager,
IGameboardRepository gameboardRepository, IGameboardRepository gameboardRepository,
IBoardManager boardManager) IActiveSessionManager boardManager)
{ {
this.logger = logger; this.logger = logger;
this.gameboardRepository = gameboardRepository; this.gameboardRepository = gameboardRepository;
@@ -37,10 +37,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
public async Task Handle(LoadGameRequest request, string userName) public async Task Handle(LoadGameRequest request, string userName)
{ {
var readSession = gameboardRepository.ReadSession(request.GameName); var sessionModel = await gameboardRepository.ReadSession(request.GameName);
var readStates = gameboardRepository.ReadBoardStates(request.GameName);
var sessionModel = await readSession;
if (sessionModel == null) if (sessionModel == null)
{ {
logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName); logger.LogWarning("{action} - {user} was unable to load session named {session}.", ClientAction.LoadGame, userName, request.GameName);
@@ -50,18 +47,13 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
} }
communicationManager.SubscribeToGame(sessionModel, userName); communicationManager.SubscribeToGame(sessionModel, userName);
var boardStates = await readStates; boardManager.Add(sessionModel);
var moveModels = boardStates
.Where(_ => _.Move != null)
.Select(_ => _.Move!.ToRulesModel())
.ToList();
var shogiBoard = new ShogiBoard(moveModels);
boardManager.Add(sessionModel.Name, shogiBoard);
var response = new LoadGameResponse(ClientAction.LoadGame) var response = new LoadGameResponse(ClientAction.LoadGame)
{ {
Game = sessionModel.ToServiceModel(), Game = new SessionMetadata(sessionModel).ToServiceModel(),
BoardState = new Models.BoardState(shogiBoard).ToServiceModel() BoardState = sessionModel.Shogi.ToServiceModel(),
MoveHistory = sessionModel.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList()
}; };
await communicationManager.BroadcastToPlayers(response, userName); await communicationManager.BroadcastToPlayers(response, userName);
} }

View File

@@ -1,7 +1,6 @@
using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Newtonsoft.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -13,35 +12,45 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
} }
public class MoveHandler : IMoveHandler public class MoveHandler : IMoveHandler
{ {
private readonly IBoardManager boardManager; private readonly IActiveSessionManager boardManager;
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardManager gameboardManager;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketConnectionManager communicationManager;
public MoveHandler( public MoveHandler(
IBoardManager boardManager, IActiveSessionManager boardManager,
ISocketCommunicationManager communicationManager, ISocketConnectionManager communicationManager,
IGameboardRepository gameboardRepository) IGameboardManager gameboardManager)
{ {
this.boardManager = boardManager; this.boardManager = boardManager;
this.gameboardRepository = gameboardRepository; this.gameboardManager = gameboardManager;
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
} }
public async Task Handle(MoveRequest request, string userName) public async Task Handle(MoveRequest request, string userName)
{ {
//var request = JsonConvert.DeserializeObject<Service.Messages.MoveRequest>(json); Move moveModel;
//var moveModel = new Move(request.Move); if (request.Move.PieceFromCaptured.HasValue)
//var board = boardManager.Get(request.GameName); {
//if (board == null) moveModel = new Move(request.Move.PieceFromCaptured.Value, request.Move.To);
//{ }
// // TODO: Find a flow for this else
// var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) {
// { moveModel = new Move(request.Move.From!, request.Move.To, request.Move.IsPromotion);
// Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." }
// };
// await communicationManager.BroadcastToPlayers(response, userName); var board = boardManager.Get(request.GameName);
if (board == null)
{
// TODO: Find a flow for this
var response = new MoveResponse(ServiceModels.Socket.Types.ClientAction.Move)
{
Error = $"Game isn't loaded. Send a message with the {ServiceModels.Socket.Types.ClientAction.LoadGame} action first."
};
await communicationManager.BroadcastToPlayers(response, userName);
}
//}
//var boardMove = moveModel.ToBoardModel();
//var moveSuccess = board.Move(boardMove); //var moveSuccess = board.Move(boardMove);
//if (moveSuccess) //if (moveSuccess)
//{ //{

View File

@@ -1,24 +1,28 @@
using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers namespace Gameboard.ShogiUI.Sockets.Managers
{ {
public interface IGameboardRepositoryManager public interface IGameboardManager
{ {
Task<string> CreateGuestUser(); Task<string> CreateGuestUser();
Task<bool> IsPlayer1(string sessionName, string playerName); Task<bool> IsPlayer1(string sessionName, string playerName);
bool IsGuest(string playerName); bool IsGuest(string playerName);
Task<bool> CreateSession(Session session); Task<bool> CreateSession(SessionMetadata session);
Task<Session?> ReadSession(string gameName);
Task<bool> UpdateSession(Session session);
Task<bool> AssignPlayer2ToSession(string sessionName, string userName);
} }
public class GameboardRepositoryManager : IGameboardRepositoryManager public class GameboardManager : IGameboardManager
{ {
private const int MaxTries = 3; private const int MaxTries = 3;
private const string GuestPrefix = "Guest-"; private const string GuestPrefix = "Guest-";
private readonly IGameboardRepository repository; private readonly IGameboardRepository repository;
public GameboardRepositoryManager(IGameboardRepository repository) public GameboardManager(IGameboardRepository repository)
{ {
this.repository = repository; this.repository = repository;
} }
@@ -53,19 +57,44 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers
//{ //{
// return await repository.PostJoinCode(sessionName, playerName); // return await repository.PostJoinCode(sessionName, playerName);
//} //}
return null; return string.Empty;
} }
public async Task<bool> CreateSession(Session session) public Task<bool> CreateSession(SessionMetadata session)
{ {
var success = await repository.CreateSession(session); return repository.CreateSession(session);
if (success)
{
return await repository.CreateBoardState(session.Name, new BoardState(), null);
}
return false;
} }
public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix);
public Task<Session?> ReadSession(string sessionName)
{
return repository.ReadSession(sessionName);
}
/// <summary>
/// Saves the session to storage.
/// </summary>
/// <param name="session">The session to save.</param>
/// <returns>True if the session was saved successfully.</returns>
public Task<bool> UpdateSession(Session session)
{
return repository.UpdateSession(session);
}
public async Task<bool> AssignPlayer2ToSession(string sessionName, string userName)
{
var isSuccess = false;
var session = await repository.ReadSession(sessionName);
if (session != null && !session.IsPrivate && string.IsNullOrEmpty(session.Player2))
{
session.SetPlayer2(userName);
if (await repository.UpdateSession(session))
{
isSuccess = true;
}
}
return isSuccess;
}
} }
} }

View File

@@ -1,140 +0,0 @@
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
using Gameboard.ShogiUI.Sockets.Managers.Utility;
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers
{
public interface ISocketCommunicationManager
{
Task BroadcastToAll(IResponse response);
//Task BroadcastToGame(string gameName, IResponse response);
//Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2);
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);
}
public class SocketCommunicationManager : ISocketCommunicationManager
{
/// <summary>Dictionary key is player name.</summary>
private readonly ConcurrentDictionary<string, WebSocket> connections;
/// <summary>Dictionary key is game name.</summary>
private readonly ConcurrentDictionary<string, Session> sessions;
private readonly ILogger<SocketCommunicationManager> logger;
public SocketCommunicationManager(ILogger<SocketCommunicationManager> logger)
{
this.logger = logger;
connections = new ConcurrentDictionary<string, WebSocket>();
sessions = new ConcurrentDictionary<string, Session>();
}
public void SubscribeToBroadcast(WebSocket socket, string playerName)
{
connections.TryAdd(playerName, socket);
}
public void UnsubscribeFromBroadcastAndGames(string playerName)
{
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)
{
var tasks = new List<Task>(playerNames.Length);
foreach (var name in playerNames)
{
if (connections.TryGetValue(name, out var socket))
{
var serialized = JsonConvert.SerializeObject(response);
logger.LogInformation("Response to {0} \n{1}\n", name, serialized);
tasks.Add(socket.SendTextAsync(serialized));
}
}
await Task.WhenAll(tasks);
}
public Task BroadcastToAll(IResponse response)
{
var message = JsonConvert.SerializeObject(response);
logger.LogInformation($"Broadcasting\n{0}", message);
var tasks = new List<Task>(connections.Count);
foreach (var kvp in connections)
{
var socket = kvp.Value;
tasks.Add(socket.SendTextAsync(message));
}
return Task.WhenAll(tasks);
}
//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;
//}
}
}

View File

@@ -1,13 +1,10 @@
using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Managers.Utility; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System.Collections.Concurrent;
using System.Net; using System.Collections.Generic;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -15,127 +12,127 @@ namespace Gameboard.ShogiUI.Sockets.Managers
{ {
public interface ISocketConnectionManager public interface ISocketConnectionManager
{ {
Task HandleSocketRequest(HttpContext context); Task BroadcastToAll(IResponse response);
//Task BroadcastToGame(string gameName, IResponse response);
//Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2);
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);
} }
/// <summary>
/// Retains all active socket connections and provides convenient methods for sending messages to clients.
/// </summary>
public class SocketConnectionManager : ISocketConnectionManager public class SocketConnectionManager : ISocketConnectionManager
{ {
/// <summary>Dictionary key is player name.</summary>
private readonly ConcurrentDictionary<string, WebSocket> connections;
/// <summary>Dictionary key is game name.</summary>
private readonly ConcurrentDictionary<string, Session> sessions;
private readonly ILogger<SocketConnectionManager> logger; private readonly ILogger<SocketConnectionManager> logger;
private readonly ISocketCommunicationManager communicationManager;
private readonly ISocketTokenManager tokenManager;
private readonly ICreateGameHandler createGameHandler;
private readonly IJoinByCodeHandler joinByCodeHandler;
private readonly IJoinGameHandler joinGameHandler;
private readonly IListGamesHandler listGamesHandler;
private readonly ILoadGameHandler loadGameHandler;
private readonly IMoveHandler moveHandler;
public SocketConnectionManager( public SocketConnectionManager(ILogger<SocketConnectionManager> logger)
ILogger<SocketConnectionManager> logger,
ISocketCommunicationManager communicationManager,
ISocketTokenManager tokenManager,
ICreateGameHandler createGameHandler,
IJoinByCodeHandler joinByCodeHandler,
IJoinGameHandler joinGameHandler,
IListGamesHandler listGamesHandler,
ILoadGameHandler loadGameHandler,
IMoveHandler moveHandler) : base()
{ {
this.logger = logger; this.logger = logger;
this.communicationManager = communicationManager; connections = new ConcurrentDictionary<string, WebSocket>();
this.tokenManager = tokenManager; sessions = new ConcurrentDictionary<string, Session>();
this.createGameHandler = createGameHandler;
this.joinByCodeHandler = joinByCodeHandler;
this.joinGameHandler = joinGameHandler;
this.listGamesHandler = listGamesHandler;
this.loadGameHandler = loadGameHandler;
this.moveHandler = moveHandler;
} }
public async Task HandleSocketRequest(HttpContext context) public void SubscribeToBroadcast(WebSocket socket, string playerName)
{ {
var hasToken = context.Request.Query.Keys.Contains("token"); connections.TryAdd(playerName, socket);
if (hasToken) }
public void UnsubscribeFromBroadcastAndGames(string playerName)
{
connections.TryRemove(playerName, out _);
foreach (var kvp in sessions)
{ {
var oneTimeToken = context.Request.Query["token"][0]; var sessionName = kvp.Key;
var tokenAsGuid = Guid.Parse(oneTimeToken); UnsubscribeFromGame(sessionName, playerName);
var userName = tokenManager.GetUsername(tokenAsGuid); }
if (userName != null) }
/// <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)
{
var tasks = new List<Task>(playerNames.Length);
foreach (var name in playerNames)
{
if (connections.TryGetValue(name, out var socket))
{ {
var socket = await context.WebSockets.AcceptWebSocketAsync(); var serialized = JsonConvert.SerializeObject(response);
logger.LogInformation("Response to {0} \n{1}\n", name, serialized);
tasks.Add(socket.SendTextAsync(serialized));
communicationManager.SubscribeToBroadcast(socket, userName);
while (!socket.CloseStatus.HasValue)
{
try
{
var message = await socket.ReceiveTextAsync();
if (string.IsNullOrWhiteSpace(message)) continue;
logger.LogInformation("Request \n{0}\n", message);
var request = JsonConvert.DeserializeObject<Request>(message);
if (!Enum.IsDefined(typeof(ClientAction), request.Action))
{
await socket.SendTextAsync("Error: Action not recognized.");
continue;
}
switch (request.Action)
{
case ClientAction.ListGames:
{
var req = JsonConvert.DeserializeObject<ListGamesRequest>(message);
await listGamesHandler.Handle(req, userName);
break;
}
case ClientAction.CreateGame:
{
var req = JsonConvert.DeserializeObject<CreateGameRequest>(message);
await createGameHandler.Handle(req, userName);
break;
}
case ClientAction.JoinGame:
{
var req = JsonConvert.DeserializeObject<JoinGameRequest>(message);
await joinGameHandler.Handle(req, userName);
break;
}
case ClientAction.JoinByCode:
{
var req = JsonConvert.DeserializeObject<JoinByCodeRequest>(message);
await joinByCodeHandler.Handle(req, userName);
break;
}
case ClientAction.LoadGame:
{
var req = JsonConvert.DeserializeObject<LoadGameRequest>(message);
await loadGameHandler.Handle(req, userName);
break;
}
case ClientAction.Move:
{
var req = JsonConvert.DeserializeObject<MoveRequest>(message);
await moveHandler.Handle(req, userName);
break;
}
}
}
catch (OperationCanceledException ex)
{
logger.LogError(ex.Message);
}
catch (WebSocketException ex)
{
logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}.");
logger.LogInformation("Probably tried writing to a closed socket.");
logger.LogError(ex.Message);
}
}
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
return;
} }
} }
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; await Task.WhenAll(tasks);
return;
} }
public Task BroadcastToAll(IResponse response)
{
var message = JsonConvert.SerializeObject(response);
logger.LogInformation($"Broadcasting\n{0}", message);
var tasks = new List<Task>(connections.Count);
foreach (var kvp in connections)
{
var socket = kvp.Value;
tasks.Add(socket.SendTextAsync(message));
}
return Task.WhenAll(tasks);
}
//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;
//}
} }
} }

View File

@@ -1,66 +0,0 @@
using Gameboard.ShogiUI.Rules;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using ServiceTypes = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Models
{
public class BoardState
{
// TODO: Create a custom 2D array implementation which removes the (x,y) or (y,x) ambiguity.
public Piece?[,] Board { get; }
public IReadOnlyCollection<Piece> Player1Hand { get; }
public IReadOnlyCollection<Piece> Player2Hand { get; }
/// <summary>
/// Move is null in the first BoardState of a Session, before any moves have been made.
/// </summary>
public Move? Move { get; }
public BoardState() : this(new ShogiBoard()) { }
public BoardState(Piece?[,] board, IList<Piece> player1Hand, ICollection<Piece> player2Hand, Move move)
{
Board = board;
Player1Hand = new ReadOnlyCollection<Piece>(player1Hand);
}
public BoardState(ShogiBoard shogi)
{
Board = new Piece[9, 9];
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var piece = shogi.Board[x, y];
if (piece != null)
{
Board[x, y] = new Piece(piece);
}
}
Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(_ => new Piece(_)).ToList();
Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(_ => new Piece(_)).ToList();
Move = new Move(shogi.MoveHistory[^1]);
}
public ServiceTypes.BoardState ToServiceModel()
{
var board = new ServiceTypes.Piece[9, 9];
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var piece = Board[x, y];
if (piece != null)
{
board[x, y] = piece.ToServiceModel();
}
}
return new ServiceTypes.BoardState
{
Board = board,
Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(),
Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList()
};
}
}
}

View File

@@ -1,41 +0,0 @@
using System;
using System.Text.RegularExpressions;
namespace Gameboard.ShogiUI.Sockets.Models
{
public class Coords
{
private const string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
private const char A = 'A';
public int X { get; }
public int Y { get; }
public Coords(int x, int y)
{
X = x;
Y = y;
}
public string ToBoardNotation()
{
var file = (char)(X + A);
var rank = Y + 1;
return $"{file}{rank}";
}
public static Coords FromBoardNotation(string notation)
{
if (string.IsNullOrEmpty(notation))
{
if (Regex.IsMatch(notation, BoardNotationRegex))
{
var match = Regex.Match(notation, BoardNotationRegex);
char file = match.Groups["file"].Value[0];
int rank = int.Parse(match.Groups["rank"].Value);
return new Coords(file - A, rank);
}
throw new ArgumentException("Board notation not recognized."); // TODO: Move this error handling to the service layer.
}
return new Coords(-1, -1); // Temporarily this is how I tell Gameboard.API that a piece came from the hand.
}
}
}

View File

@@ -1,41 +1,86 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using System;
using System.Diagnostics;
using System.Numerics; using System.Numerics;
using System.Text.RegularExpressions;
namespace Gameboard.ShogiUI.Sockets.Models namespace Gameboard.ShogiUI.Sockets.Models
{ {
[DebuggerDisplay("{From} - {To}")]
public class Move public class Move
{ {
public Coords? From { get; set; } private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
public bool IsPromotion { get; set; } private static readonly char A = 'A';
public WhichPiece? PieceFromHand { get; set; }
public Coords To { get; set; }
public Move(Coords from, Coords to, bool isPromotion) public Vector2? From { get; }
public bool IsPromotion { get; }
public WhichPiece? PieceFromHand { get; }
public Vector2 To { get; }
public Move(Vector2 from, Vector2 to, bool isPromotion = false)
{ {
From = from; From = from;
To = to; To = to;
IsPromotion = isPromotion; IsPromotion = isPromotion;
} }
public Move(WhichPiece pieceFromHand, Vector2 to)
public Move(WhichPiece pieceFromHand, Coords to)
{ {
PieceFromHand = pieceFromHand; PieceFromHand = pieceFromHand;
To = to; To = to;
} }
/// <summary>
/// Constructor to represent moving a piece on the Board to another position on the Board.
/// </summary>
/// <param name="fromNotation">Position the piece is being moved from.</param>
/// <param name="toNotation">Position the piece is being moved to.</param>
/// <param name="isPromotion">If the moving piece should be promoted.</param>
public Move(string fromNotation, string toNotation, bool isPromotion = false)
{
From = FromBoardNotation(fromNotation);
To = FromBoardNotation(toNotation);
IsPromotion = isPromotion;
}
/// <summary>
/// Constructor to represent moving a piece from the Hand to the Board.
/// </summary>
/// <param name="pieceFromHand">The piece being moved from the Hand to the Board.</param>
/// <param name="toNotation">Position the piece is being moved to.</param>
/// <param name="isPromotion">If the moving piece should be promoted.</param>
public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false)
{
From = null;
PieceFromHand = pieceFromHand;
To = FromBoardNotation(toNotation);
IsPromotion = isPromotion;
}
public ServiceModels.Socket.Types.Move ToServiceModel() => new() public ServiceModels.Socket.Types.Move ToServiceModel() => new()
{ {
From = From?.ToBoardNotation(), From = From.HasValue ? ToBoardNotation(From.Value) : null,
IsPromotion = IsPromotion, IsPromotion = IsPromotion,
To = To.ToBoardNotation(), PieceFromCaptured = PieceFromHand.HasValue ? PieceFromHand : null,
PieceFromCaptured = PieceFromHand To = ToBoardNotation(To)
}; };
public Rules.Move ToRulesModel() private static string ToBoardNotation(Vector2 vector)
{ {
return PieceFromHand != null var file = (char)(vector.X + A);
? new Rules.Move((Rules.WhichPiece)PieceFromHand, new Vector2(To.X, To.Y)) var rank = vector.Y + 1;
: new Rules.Move(new Vector2(From!.X, From.Y), new Vector2(To.X, To.Y), IsPromotion); return $"{file}{rank}";
}
private static Vector2 FromBoardNotation(string notation)
{
if (Regex.IsMatch(notation, BoardNotationRegex))
{
var match = Regex.Match(notation, BoardNotationRegex);
char file = match.Groups["file"].Value[0];
int rank = int.Parse(match.Groups["rank"].Value);
return new Vector2(file - A, rank);
}
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
} }
} }
} }

View File

@@ -0,0 +1,95 @@
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)
};
}
}

View File

@@ -1,27 +1,59 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using PathFinding;
using System.Diagnostics;
namespace Gameboard.ShogiUI.Sockets.Models namespace Gameboard.ShogiUI.Sockets.Models
{ {
public class Piece [DebuggerDisplay("{WhichPiece} {Owner}")]
public class Piece : IPlanarElement
{ {
public bool IsPromoted { get; }
public WhichPlayer Owner { get; }
public WhichPiece WhichPiece { get; } public WhichPiece WhichPiece { get; }
public WhichPlayer Owner { get; private set; }
public bool IsPromoted { get; private set; }
public bool IsUpsideDown => Owner == WhichPlayer.Player2;
public Piece(bool isPromoted, WhichPlayer owner, WhichPiece whichPiece) public Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false)
{ {
IsPromoted = isPromoted; WhichPiece = piece;
Owner = owner; Owner = owner;
WhichPiece = whichPiece; IsPromoted = isPromoted;
} }
public Piece(Rules.Pieces.Piece piece) public bool CanPromote => !IsPromoted
&& WhichPiece != WhichPiece.King
&& WhichPiece != WhichPiece.GoldGeneral;
public void ToggleOwnership()
{ {
IsPromoted = piece.IsPromoted; Owner = Owner == WhichPlayer.Player1
Owner = (WhichPlayer)piece.Owner; ? WhichPlayer.Player2
WhichPiece = (WhichPiece)piece.WhichPiece; : WhichPlayer.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.Socket.Types.Piece ToServiceModel() public ServiceModels.Socket.Types.Piece ToServiceModel()
{ {
return new ServiceModels.Socket.Types.Piece return new ServiceModels.Socket.Types.Piece

View File

@@ -1,12 +0,0 @@
namespace Gameboard.ShogiUI.Sockets.Models
{
public class Player
{
public string Name { get; }
public Player(string name)
{
Name = name;
}
}
}

View File

@@ -1,22 +1,21 @@
using Gameboard.ShogiUI.Sockets.Extensions; using Newtonsoft.Json;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Newtonsoft.Json;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Models namespace Gameboard.ShogiUI.Sockets.Models
{ {
public class Session public class Session
{ {
// TODO: Separate subscriptions to the Session from the Session.
[JsonIgnore] public ConcurrentDictionary<string, WebSocket> Subscriptions { get; } [JsonIgnore] public ConcurrentDictionary<string, WebSocket> Subscriptions { get; }
public string Name { get; } public string Name { get; }
public string Player1 { get; } public string Player1 { get; }
public string? Player2 { get; } public string? Player2 { get; private set; }
public bool IsPrivate { get; } public bool IsPrivate { get; }
public Session(string name, bool isPrivate, string player1, string? player2 = null) public Shogi Shogi { get; }
public Session(string name, bool isPrivate, Shogi shogi, string player1, string? player2 = null)
{ {
Subscriptions = new ConcurrentDictionary<string, WebSocket>(); Subscriptions = new ConcurrentDictionary<string, WebSocket>();
@@ -24,48 +23,12 @@ namespace Gameboard.ShogiUI.Sockets.Models
Player1 = player1; Player1 = player1;
Player2 = player2; Player2 = player2;
IsPrivate = isPrivate; IsPrivate = isPrivate;
Shogi = shogi;
} }
public bool Subscribe(string playerName, WebSocket socket) => Subscriptions.TryAdd(playerName, socket); public void SetPlayer2(string userName)
public Task Broadcast(string message)
{ {
var tasks = new List<Task>(Subscriptions.Count); Player2 = userName;
foreach (var kvp in Subscriptions)
{
var socket = kvp.Value;
tasks.Add(socket.SendTextAsync(message));
}
return Task.WhenAll(tasks);
}
public Task SendToPlayer1(string message)
{
if (Subscriptions.TryGetValue(Player1, out var socket))
{
return socket.SendTextAsync(message);
}
return Task.CompletedTask;
}
public Task SendToPlayer2(string message)
{
if (Player2 != null && Subscriptions.TryGetValue(Player2, out var socket))
{
return socket.SendTextAsync(message);
}
return Task.CompletedTask;
}
public Game ToServiceModel()
{
var players = new List<string>(2) { Player1 };
if (!string.IsNullOrWhiteSpace(Player2)) players.Add(Player2);
return new Game
{
GameName = Name,
Players = players.ToArray()
};
} }
} }
} }

View File

@@ -0,0 +1,30 @@
namespace Gameboard.ShogiUI.Sockets.Models
{
/// <summary>
/// A representation of a Session without the board and game-rules.
/// </summary>
public class SessionMetadata
{
public string Name { get; }
public string Player1 { get; }
public string? Player2 { get; }
public bool IsPrivate { get; }
public SessionMetadata(string name, bool isPrivate, string player1, string? player2)
{
Name = name;
IsPrivate = isPrivate;
Player1 = player1;
Player2 = player2;
}
public SessionMetadata(Session sessionModel)
{
Name = sessionModel.Name;
IsPrivate = sessionModel.IsPrivate;
Player1 = sessionModel.Player1;
Player2 = sessionModel.Player2;
}
public ServiceModels.Socket.Types.Game ToServiceModel() => new(Name, Player1, Player2);
}
}

View File

@@ -1,21 +1,22 @@
using Gameboard.ShogiUI.Rules.Pieces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using PathFinding; using PathFinding;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics; using System.Numerics;
namespace Gameboard.ShogiUI.Rules namespace Gameboard.ShogiUI.Sockets.Models
{ {
/// <summary> /// <summary>
/// Facilitates Shogi board state transitions, cognisant of Shogi rules. /// Facilitates Shogi board state transitions, cognisant of Shogi rules.
/// 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 class ShogiBoard public class Shogi
{ {
private delegate void MoveSetCallback(Piece piece, Vector2 position); private delegate void MoveSetCallback(Piece piece, Vector2 position);
private readonly PathFinder2D<Piece> pathFinder; private readonly PathFinder2D<Piece> pathFinder;
private ShogiBoard? validationBoard; private Shogi? validationBoard;
private Vector2 player1King; private Vector2 player1King;
private Vector2 player2King; private Vector2 player2King;
public IReadOnlyDictionary<WhichPlayer, List<Piece>> Hands { get; } public IReadOnlyDictionary<WhichPlayer, List<Piece>> Hands { get; }
@@ -27,7 +28,7 @@ namespace Gameboard.ShogiUI.Rules
public string Error { get; private set; } public string Error { get; private set; }
public ShogiBoard() public Shogi()
{ {
Board = new PlanarCollection<Piece>(9, 9); Board = new PlanarCollection<Piece>(9, 9);
MoveHistory = new List<Move>(20); MoveHistory = new List<Move>(20);
@@ -36,13 +37,14 @@ namespace Gameboard.ShogiUI.Rules
{ WhichPlayer.Player2, new List<Piece>()}, { WhichPlayer.Player2, new List<Piece>()},
}; };
pathFinder = new PathFinder2D<Piece>(Board); pathFinder = new PathFinder2D<Piece>(Board);
InitializeBoardState();
player1King = new Vector2(4, 8); player1King = new Vector2(4, 8);
player2King = new Vector2(4, 0); player2King = new Vector2(4, 0);
Error = string.Empty; Error = string.Empty;
InitializeBoardState();
} }
public ShogiBoard(IList<Move> moves) : this() public Shogi(IList<Move> moves) : this()
{ {
for (var i = 0; i < moves.Count; i++) for (var i = 0; i < moves.Count; i++)
{ {
@@ -54,16 +56,16 @@ namespace Gameboard.ShogiUI.Rules
} }
} }
private ShogiBoard(ShogiBoard toCopy) private Shogi(Shogi toCopy)
{ {
Board = new PlanarCollection<Piece>(9, 9); Board = new PlanarCollection<Piece>(9, 9);
for (var x = 0; x < 9; x++) for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++) for (var y = 0; y < 9; y++)
{ {
var piece = toCopy.Board[x, y]; var piece = toCopy.Board[y, x];
if (piece != null) if (piece != null)
{ {
Board[x, y] = piece.DeepClone(); Board[y, x] = new Piece(piece.WhichPiece, piece.Owner, piece.IsPromoted);
} }
} }
@@ -105,7 +107,7 @@ namespace Gameboard.ShogiUI.Rules
// Try making the move in a "throw away" board. // Try making the move in a "throw away" board.
if (validationBoard == null) if (validationBoard == null)
{ {
validationBoard = new ShogiBoard(this); validationBoard = new Shogi(this);
} }
var isValid = move.PieceFromHand.HasValue var isValid = move.PieceFromHand.HasValue
@@ -138,7 +140,7 @@ namespace Gameboard.ShogiUI.Rules
if (move.PieceFromHand.HasValue == false) return false; //Invalid move if (move.PieceFromHand.HasValue == false) return false; //Invalid move
var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand); var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromHand);
if (index < 0) return false; // Invalid move if (index < 0) return false; // Invalid move
if (Board[move.To.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand. if (Board[move.To.Y, move.To.X] != null) return false; // Invalid move; cannot capture while playing from the hand.
var minimumY = 0; var minimumY = 0;
switch (move.PieceFromHand.Value) switch (move.PieceFromHand.Value)
@@ -157,7 +159,7 @@ namespace Gameboard.ShogiUI.Rules
if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false; if (WhoseTurn == WhichPlayer.Player2 && move.To.Y > minimumY) return false;
// Mutate the board. // Mutate the board.
Board[move.To.X, move.To.Y] = Hands[WhoseTurn][index]; Board[move.To.Y, move.To.X] = Hands[WhoseTurn][index];
Hands[WhoseTurn].RemoveAt(index); Hands[WhoseTurn].RemoveAt(index);
return true; return true;
@@ -165,7 +167,7 @@ namespace Gameboard.ShogiUI.Rules
/// <returns>True if the move was successful.</returns> /// <returns>True if the move was successful.</returns>
private bool PlaceFromBoard(Move move) private bool PlaceFromBoard(Move move)
{ {
var fromPiece = Board[move.From.Value.X, move.From.Value.Y]; var fromPiece = Board[move.From.Value.Y, move.From.Value.X];
if (fromPiece == null) if (fromPiece == null)
{ {
Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}."; Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}.";
@@ -182,7 +184,7 @@ namespace Gameboard.ShogiUI.Rules
return false; // Invalid move; move not part of move-set. return false; // Invalid move; move not part of move-set.
} }
var captured = Board[move.To.X, move.To.Y]; var captured = Board[move.To.Y, move.To.X];
if (captured != null) if (captured != null)
{ {
if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece. if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece.
@@ -202,8 +204,8 @@ namespace Gameboard.ShogiUI.Rules
fromPiece.Promote(); fromPiece.Promote();
} }
} }
Board[move.To.X, move.To.Y] = fromPiece; Board[move.To.Y, move.To.X] = fromPiece;
Board[move.From.Value.X, move.From.Value.Y] = null; Board[move.From.Value.Y, move.From.Value.X] = null;
if (fromPiece.WhichPiece == WhichPiece.King) if (fromPiece.WhichPiece == WhichPiece.King)
{ {
if (fromPiece.Owner == WhichPlayer.Player1) if (fromPiece.Owner == WhichPlayer.Player1)
@@ -223,7 +225,7 @@ namespace Gameboard.ShogiUI.Rules
private bool IsPathable(Vector2 from, Vector2 to) private bool IsPathable(Vector2 from, Vector2 to)
{ {
var piece = Board[from.X, from.Y]; var piece = Board[from.Y, from.X];
if (piece == null) return false; if (piece == null) return false;
var isObstructed = false; var isObstructed = false;
@@ -308,8 +310,8 @@ namespace Gameboard.ShogiUI.Rules
// ...evaluate if any move gets the player out of check. // ...evaluate if any move gets the player out of check.
pathFinder.PathEvery(from, (other, position) => pathFinder.PathEvery(from, (other, position) =>
{ {
if (validationBoard == null) validationBoard = new ShogiBoard(this); if (validationBoard == null) validationBoard = new Shogi(this);
var moveToTry = new Move(from, position, false); var moveToTry = new Move(from, position);
var moveSuccess = validationBoard.TryMove(moveToTry); var moveSuccess = validationBoard.TryMove(moveToTry);
if (moveSuccess) if (moveSuccess)
{ {
@@ -331,44 +333,44 @@ namespace Gameboard.ShogiUI.Rules
{ {
for (int y = 3; y < 6; y++) for (int y = 3; y < 6; y++)
for (int x = 0; x < 9; x++) for (int x = 0; x < 9; x++)
Board[x, y] = null; Board[y, x] = null;
} }
private void ResetFrontRow(WhichPlayer player) private void ResetFrontRow(WhichPlayer player)
{ {
int y = player == WhichPlayer.Player1 ? 6 : 2; int y = player == WhichPlayer.Player1 ? 6 : 2;
for (int x = 0; x < 9; x++) Board[x, y] = new Pawn(player); for (int x = 0; x < 9; x++) Board[y, x] = new Piece(WhichPiece.Pawn, player);
} }
private void ResetMiddleRow(WhichPlayer player) private void ResetMiddleRow(WhichPlayer player)
{ {
int y = player == WhichPlayer.Player1 ? 7 : 1; int y = player == WhichPlayer.Player1 ? 7 : 1;
Board[0, y] = null; Board[y, 0] = null;
for (int x = 2; x < 7; x++) Board[x, y] = null; for (int x = 2; x < 7; x++) Board[y, x] = null;
Board[8, y] = null; Board[y, 8] = null;
if (player == WhichPlayer.Player1) if (player == WhichPlayer.Player1)
{ {
Board[1, y] = new Bishop(player); Board[y, 1] = new Piece(WhichPiece.Bishop, player);
Board[7, y] = new Rook(player); Board[y, 7] = new Piece(WhichPiece.Rook, player);
} }
else else
{ {
Board[1, y] = new Rook(player); Board[y, 1] = new Piece(WhichPiece.Rook, player);
Board[7, y] = new Bishop(player); Board[y, 7] = new Piece(WhichPiece.Bishop, player);
} }
} }
private void ResetRearRow(WhichPlayer player) private void ResetRearRow(WhichPlayer player)
{ {
int y = player == WhichPlayer.Player1 ? 8 : 0; int y = player == WhichPlayer.Player1 ? 8 : 0;
Board[0, y] = new Lance(player); Board[y, 0] = new Piece(WhichPiece.Lance, player);
Board[1, y] = new Knight(player); Board[y, 1] = new Piece(WhichPiece.Knight, player);
Board[2, y] = new SilverGeneral(player); Board[y, 2] = new Piece(WhichPiece.SilverGeneral, player);
Board[3, y] = new GoldenGeneral(player); Board[y, 3] = new Piece(WhichPiece.GoldGeneral, player);
Board[4, y] = new King(player); Board[y, 4] = new Piece(WhichPiece.King, player);
Board[5, y] = new GoldenGeneral(player); Board[y, 5] = new Piece(WhichPiece.GoldGeneral, player);
Board[6, y] = new SilverGeneral(player); Board[y, 6] = new Piece(WhichPiece.SilverGeneral, player);
Board[7, y] = new Knight(player); Board[y, 7] = new Piece(WhichPiece.Knight, player);
Board[8, y] = new Lance(player); Board[y, 8] = new Piece(WhichPiece.Lance, player);
} }
private void InitializeBoardState() private void InitializeBoardState()
{ {
@@ -381,5 +383,28 @@ namespace Gameboard.ShogiUI.Rules
ResetRearRow(WhichPlayer.Player1); ResetRearRow(WhichPlayer.Player1);
} }
#endregion #endregion
public BoardState ToServiceModel()
{
var board = new ServiceModels.Socket.Types.Piece[9, 9];
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var piece = Board[y, x];
if (piece != null)
{
board[y, x] = piece.ToServiceModel();
}
}
return new BoardState
{
Board = board,
PlayerInCheck = InCheck,
WhoseTurn = WhoseTurn,
Player1Hand = Hands[WhichPlayer.Player1].Select(_ => _.ToServiceModel()).ToList(),
Player2Hand = Hands[WhichPlayer.Player2].Select(_ => _.ToServiceModel()).ToList()
};
}
} }
} }

View File

@@ -1,75 +0,0 @@
using System;
using System.Linq;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class BoardState : CouchDocument
{
public string Name { get; set; }
public Piece?[,] Board { get; set; }
public Piece[] Player1Hand { get; set; }
public Piece[] Player2Hand { get; set; }
/// <summary>
/// Move is null for first BoardState of a session - before anybody has made moves.
/// </summary>
public Move? Move { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public BoardState() : base()
{
Name = string.Empty;
Board = new Piece[9, 9];
Player1Hand = Array.Empty<Piece>();
Player2Hand = Array.Empty<Piece>();
}
public BoardState(string sessionName, Models.BoardState boardState) : base($"{sessionName}-{DateTime.Now:O}", nameof(BoardState))
{
Name = sessionName;
Board = new Piece[9, 9];
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var piece = boardState.Board[x, y];
if (piece != null)
{
Board[x, y] = new Piece(piece);
}
}
Player1Hand = boardState.Player1Hand.Select(model => new Piece(model)).ToArray();
Player2Hand = boardState.Player2Hand.Select(model => new Piece(model)).ToArray();
if (boardState.Move != null)
{
Move = new Move(boardState.Move);
}
}
public Models.BoardState ToDomainModel()
{
/*
* Board = new Piece[9, 9];
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var piece = boardState.Board[x, y];
if (piece != null)
{
Board[x, y] = new Piece(piece);
}
}
Player1Hand = boardState.Player1Hand.Select(_ => new Piece(_)).ToList();
Player2Hand = boardState.Player2Hand.Select(_ => new Piece(_)).ToList();
if (boardState.Move != null)
{
Move = new Move(boardState.Move);
}
*/
return null;
}
}
}

View File

@@ -0,0 +1,57 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using System;
using System.Linq;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class BoardStateDocument : CouchDocument
{
public string Name { get; set; }
public Piece?[,] Board { get; set; }
public Piece[] Player1Hand { get; set; }
public Piece[] Player2Hand { get; set; }
/// <summary>
/// Move is null for first BoardState of a session - before anybody has made moves.
/// </summary>
public Move? Move { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public BoardStateDocument() : base(WhichDocumentType.BoardState)
{
Name = string.Empty;
Board = new Piece[9, 9];
Player1Hand = Array.Empty<Piece>();
Player2Hand = Array.Empty<Piece>();
}
public BoardStateDocument(string sessionName, Models.Shogi shogi)
: base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState)
{
Name = sessionName;
Board = new Piece[9, 9];
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var piece = shogi.Board[y, x];
if (piece != null)
{
Board[y, x] = new Piece(piece);
}
}
Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(model => new Piece(model)).ToArray();
Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(model => new Piece(model)).ToArray();
if (shogi.MoveHistory.Count > 0)
{
Move = new Move(shogi.MoveHistory[^1]);
}
}
}
}

View File

@@ -5,21 +5,21 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{ {
public abstract class CouchDocument public abstract class CouchDocument
{ {
[JsonProperty("_id")] [JsonProperty("_id")] public string Id { get; set; }
public string Id { get; set; } public WhichDocumentType DocumentType { get; }
public string Type { get; set; }
public DateTimeOffset CreatedDate { get; set; } public DateTimeOffset CreatedDate { get; set; }
public CouchDocument() public CouchDocument(WhichDocumentType documentType)
{ : this(string.Empty, documentType, DateTimeOffset.UtcNow) { }
Id = string.Empty;
Type = string.Empty; public CouchDocument(string id, WhichDocumentType documentType)
CreatedDate = DateTimeOffset.UtcNow; : this(id, documentType, DateTimeOffset.UtcNow) { }
}
public CouchDocument(string id, string type) public CouchDocument(string id, WhichDocumentType documentType, DateTimeOffset createdDate)
{ {
Id = id; Id = id;
Type = type; DocumentType = documentType;
CreatedDate = createdDate;
} }
} }
} }

View File

@@ -1,5 +1,5 @@
using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using System.Numerics;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{ {
@@ -32,14 +32,25 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
public Move(Models.Move move) public Move(Models.Move move)
{ {
From = move.From?.ToBoardNotation(); if (move.From.HasValue)
{
From = ToBoardNotation(move.From.Value);
}
IsPromotion = move.IsPromotion; IsPromotion = move.IsPromotion;
To = move.To.ToBoardNotation(); To = ToBoardNotation(move.To);
PieceFromHand = move.PieceFromHand; 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 public Models.Move ToDomainModel() => PieceFromHand.HasValue
? new((ServiceModels.Socket.Types.WhichPiece)PieceFromHand, Coords.FromBoardNotation(To)) ? new(PieceFromHand.Value, To)
: new(Coords.FromBoardNotation(From!), Coords.FromBoardNotation(To), IsPromotion); : new(From!, To, IsPromotion);
} }
} }

View File

@@ -22,6 +22,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
WhichPiece = piece.WhichPiece; WhichPiece = piece.WhichPiece;
} }
public Models.Piece ToDomainModel() => new(IsPromoted, Owner, WhichPiece); public Models.Piece ToDomainModel() => new(WhichPiece, Owner, IsPromoted);
} }
} }

View File

@@ -1,4 +0,0 @@
### Couch Models
Couch models should accept domain models during construction and offer a ToDomainModel method which constructs a domain model.
In this way, domain models have the freedom to define their valid states.

View File

@@ -1,30 +0,0 @@
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class Session : CouchDocument
{
public string Name { get; set; }
public string Player1 { get; set; }
public string? Player2 { get; set; }
public bool IsPrivate { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public Session() : base()
{
Name = string.Empty;
Player1 = string.Empty;
Player2 = string.Empty;
}
public Session(string id, Models.Session session) : base(id, nameof(Session))
{
Name = session.Name;
Player1 = session.Player1;
Player2 = session.Player2;
IsPrivate = session.IsPrivate;
}
public Models.Session ToDomainModel() => new(Name, IsPrivate, Player1, Player2);
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class SessionDocument : CouchDocument
{
public string Name { get; set; }
public string Player1 { get; set; }
public string? Player2 { get; set; }
public bool IsPrivate { get; set; }
public IList<BoardStateDocument> History { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public SessionDocument() : base(WhichDocumentType.Session)
{
Name = string.Empty;
Player1 = string.Empty;
Player2 = string.Empty;
History = new List<BoardStateDocument>(0);
}
public SessionDocument(Models.Session session)
: base(session.Name, WhichDocumentType.Session)
{
Name = session.Name;
Player1 = session.Player1;
Player2 = session.Player2;
IsPrivate = session.IsPrivate;
History = new List<BoardStateDocument>(0);
}
public SessionDocument(Models.SessionMetadata sessionMetaData)
: base(sessionMetaData.Name, WhichDocumentType.Session)
{
Name = sessionMetaData.Name;
Player1 = sessionMetaData.Player1;
Player2 = sessionMetaData.Player2;
IsPrivate = sessionMetaData.IsPrivate;
History = new List<BoardStateDocument>(0);
}
public Models.Session ToDomainModel(Models.Shogi shogi) => new(Name, IsPrivate, shogi, Player1, Player2);
public Models.SessionMetadata ToDomainModel() => new(Name, IsPrivate, Player1, Player2);
}
}

View File

@@ -1,8 +1,6 @@
using System; namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{ {
public class User : CouchDocument public class UserDocument : CouchDocument
{ {
public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}"; public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}";
@@ -14,7 +12,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
public string Name { get; set; } public string Name { get; set; }
public LoginPlatform Platform { get; set; } public LoginPlatform Platform { get; set; }
public User(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", nameof(User)) public UserDocument(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", WhichDocumentType.User)
{ {
Name = name; Name = name;
Platform = platform; Platform = platform;

View File

@@ -0,0 +1,9 @@
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public enum WhichDocumentType
{
User,
Session,
BoardState
}
}

View File

@@ -12,14 +12,14 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
{ {
public interface IGameboardRepository public interface IGameboardRepository
{ {
Task<bool> CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move);
Task<bool> CreateGuestUser(string userName); Task<bool> CreateGuestUser(string userName);
Task<bool> CreateSession(Models.Session session); Task<bool> CreateSession(Models.SessionMetadata session);
Task<IList<Models.Session>> ReadSessions(); Task<IList<Models.SessionMetadata>> ReadSessionMetadatas();
Task<bool> IsGuestUser(string userName); Task<bool> IsGuestUser(string userName);
Task<string> PostJoinCode(string gameName, string userName); Task<string> PostJoinCode(string gameName, string userName);
Task<Models.Session?> ReadSession(string name); Task<Models.Session?> ReadSession(string name);
Task<IList<Models.BoardState>> ReadBoardStates(string name); Task<Models.Shogi?> ReadShogi(string name);
Task<bool> UpdateSession(Models.Session session);
} }
public class GameboardRepository : IGameboardRepository public class GameboardRepository : IGameboardRepository
@@ -34,75 +34,99 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
this.logger = logger; this.logger = logger;
} }
public async Task<IList<Models.Session>> ReadSessions() public async Task<IList<Models.SessionMetadata>> ReadSessionMetadatas()
{ {
var selector = $@"{{ ""{nameof(Session.Type)}"": ""{nameof(Session)}"" }}"; var selector = new Dictionary<string, object>(2)
var query = $@"{{ ""selector"": {selector} }}"; {
var content = new StringContent(query, Encoding.UTF8, ApplicationJson); [nameof(SessionDocument.DocumentType)] = WhichDocumentType.Session
};
var q = new { Selector = selector };
var content = new StringContent(JsonConvert.SerializeObject(q), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content); var response = await client.PostAsync("_find", content);
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchFindResult<Session>>(responseContent); var sessions = JsonConvert.DeserializeObject<CouchFindResult<SessionDocument>>(responseContent).docs;
if (result == null) return sessions
{ .Select(s => new Models.SessionMetadata(s.Name, s.IsPrivate, s.Player1, s.Player2))
logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions));
return Array.Empty<Models.Session>();
}
return result.docs
.Select(_ => _.ToDomainModel())
.ToList(); .ToList();
} }
public async Task<Models.Session?> ReadSession(string name) public async Task<Models.Session?> ReadSession(string name)
{ {
var readShogiTask = ReadShogi(name);
var response = await client.GetAsync(name); var response = await client.GetAsync(name);
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
var couchModel = JsonConvert.DeserializeObject<Session>(responseContent); var couchModel = JsonConvert.DeserializeObject<SessionDocument>(responseContent);
return couchModel.ToDomainModel(); var shogi = await readShogiTask;
if (shogi == null)
{
return null;
}
return couchModel.ToDomainModel(shogi);
} }
public async Task<IList<Models.BoardState>> ReadBoardStates(string name) public async Task<Models.Shogi?> ReadShogi(string name)
{ {
var selector = $@"{{ ""{nameof(BoardState.Type)}"": ""{nameof(BoardState)}"", ""{nameof(BoardState.Name)}"": ""{name}"" }}"; var selector = new Dictionary<string, object>(2)
var sort = $@"{{ ""{nameof(BoardState.CreatedDate)}"" : ""desc"" }}"; {
var query = $@"{{ ""selector"": {selector}, ""sort"": {sort} }}"; [nameof(BoardStateDocument.DocumentType)] = WhichDocumentType.BoardState,
[nameof(BoardStateDocument.Name)] = name
};
var sort = new Dictionary<string, object>(1)
{
[nameof(BoardStateDocument.CreatedDate)] = "desc"
};
var query = JsonConvert.SerializeObject(new { selector, sort = new[] { sort } });
var content = new StringContent(query, Encoding.UTF8, ApplicationJson); var content = new StringContent(query, Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content); var response = await client.PostAsync("_find", content);
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchFindResult<BoardState>>(responseContent); if (!response.IsSuccessStatusCode)
if (result == null)
{ {
logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions)); logger.LogError("Couch error during _find in {func}: {error}.\n\nQuery: {query}", nameof(ReadShogi), responseContent, query);
return Array.Empty<Models.BoardState>(); return null;
} }
return result.docs var boardStates = JsonConvert
.Select(_ => new Models.BoardState(_)) .DeserializeObject<CouchFindResult<BoardStateDocument>>(responseContent)
.ToList(); .docs;
if (boardStates.Length == 0) return null;
// Skip(1) because the first BoardState has no move; it represents the initial board state of a new Session.
var moves = boardStates.Skip(1).Select(couchModel =>
{
var move = couchModel.Move;
Models.Move model = move!.PieceFromHand.HasValue
? new Models.Move(move.PieceFromHand.Value, move.To)
: new Models.Move(move.From!, move.To, move.IsPromotion);
return model;
}).ToList();
return new Models.Shogi(moves);
} }
//public async Task DeleteGame(string gameName) public async Task<bool> CreateSession(Models.SessionMetadata session)
//{
// //var uri = $"Session/{gameName}";
// //await client.DeleteAsync(Uri.EscapeUriString(uri));
//}
public async Task<bool> CreateSession(Models.Session session)
{ {
var couchModel = new Session(session.Name, session); var sessionDocument = new SessionDocument(session);
var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent);
var boardStateDocument = new BoardStateDocument(session.Name, new Models.Shogi());
var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
if ((await postSessionDocumentTask).IsSuccessStatusCode)
{
var response = await client.PostAsync(string.Empty, boardStateContent);
return response.IsSuccessStatusCode;
}
return false;
}
public async Task<bool> UpdateSession(Models.Session session)
{
var couchModel = new SessionDocument(session);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content); var response = await client.PutAsync(couchModel.Id, content);
return response.IsSuccessStatusCode; return response.IsSuccessStatusCode;
} }
public async Task<bool> CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move)
{
var couchModel = new BoardState(sessionName, boardState, move);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
}
//public async Task<bool> PutJoinPublicSession(PutJoinPublicSession request) //public async Task<bool> PutJoinPublicSession(PutJoinPublicSession request)
//{ //{
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
@@ -169,7 +193,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
public async Task<bool> CreateGuestUser(string userName) public async Task<bool> CreateGuestUser(string userName)
{ {
var couchModel = new User(userName, User.LoginPlatform.Guest); var couchModel = new UserDocument(userName, UserDocument.LoginPlatform.Guest);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content); var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode; return response.IsSuccessStatusCode;
@@ -177,7 +201,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
public async Task<bool> IsGuestUser(string userName) public async Task<bool> IsGuestUser(string userName)
{ {
var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{User.GetDocumentId(userName)}")); var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{UserDocument.GetDocumentId(userName)}"));
var response = await client.SendAsync(req); var response = await client.SendAsync(req);
return response.IsSuccessStatusCode; return response.IsSuccessStatusCode;
} }

View File

@@ -0,0 +1,15 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{
public class CreateGameRequestValidator : AbstractValidator<CreateGameRequest>
{
public CreateGameRequestValidator()
{
RuleFor(_ => _.Action).Equal(ClientAction.CreateGame);
RuleFor(_ => _.GameName).NotEmpty();
}
}
}

View File

@@ -0,0 +1,15 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{
public class JoinByCodeRequestValidator : AbstractValidator<JoinByCodeRequest>
{
public JoinByCodeRequestValidator()
{
RuleFor(_ => _.Action).Equal(ClientAction.JoinByCode);
RuleFor(_ => _.JoinCode).NotEmpty();
}
}
}

View File

@@ -0,0 +1,15 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{
public class JoinGameRequestValidator : AbstractValidator<JoinGameRequest>
{
public JoinGameRequestValidator()
{
RuleFor(_ => _.Action).Equal(ClientAction.JoinGame);
RuleFor(_ => _.GameName).NotEmpty();
}
}
}

View File

@@ -0,0 +1,14 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{
public class ListGamesRequestValidator : AbstractValidator<ListGamesRequest>
{
public ListGamesRequestValidator()
{
RuleFor(_ => _.Action).Equal(ClientAction.ListGames);
}
}
}

View File

@@ -0,0 +1,15 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{
public class LoadGameRequestValidator : AbstractValidator<LoadGameRequest>
{
public LoadGameRequestValidator()
{
RuleFor(_ => _.Action).Equal(ClientAction.LoadGame);
RuleFor(_ => _.GameName).NotEmpty();
}
}
}

View File

@@ -0,0 +1,23 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
{
public class MoveRequestValidator : AbstractValidator<MoveRequest>
{
public MoveRequestValidator()
{
RuleFor(_ => _.Action).Equal(ClientAction.Move);
RuleFor(_ => _.GameName).NotEmpty();
RuleFor(_ => _.Move.From)
.Null()
.When(_ => _.Move.PieceFromCaptured.HasValue)
.WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties.");
RuleFor(_ => _.Move.From)
.NotEmpty()
.When(_ => !_.Move.PieceFromCaptured.HasValue)
.WithMessage("Move.From and Move.PieceFromCaptured are mutually exclusive properties.");
}
}
}

View File

@@ -0,0 +1,194 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
using Gameboard.ShogiUI.Sockets.Managers.Utility;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Services
{
public interface ISocketService
{
Task HandleSocketRequest(HttpContext context);
}
/// <summary>
/// Services a single websocket connection. Authenticates the socket connection, accepts messages, and sends messages.
/// </summary>
public class SocketService : ISocketService
{
private readonly ILogger<SocketService> logger;
private readonly ISocketConnectionManager communicationManager;
private readonly ISocketTokenManager tokenManager;
private readonly ICreateGameHandler createGameHandler;
private readonly IJoinByCodeHandler joinByCodeHandler;
private readonly IJoinGameHandler joinGameHandler;
private readonly IListGamesHandler listGamesHandler;
private readonly ILoadGameHandler loadGameHandler;
private readonly IMoveHandler moveHandler;
private readonly IValidator<CreateGameRequest> createGameRequestValidator;
private readonly IValidator<JoinByCodeRequest> joinByCodeRequestValidator;
private readonly IValidator<JoinGameRequest> joinGameRequestValidator;
private readonly IValidator<ListGamesRequest> listGamesRequestValidator;
private readonly IValidator<LoadGameRequest> loadGameRequestValidator;
private readonly IValidator<MoveRequest> moveRequestValidator;
public SocketService(
ILogger<SocketService> logger,
ISocketConnectionManager communicationManager,
ISocketTokenManager tokenManager,
ICreateGameHandler createGameHandler,
IJoinByCodeHandler joinByCodeHandler,
IJoinGameHandler joinGameHandler,
IListGamesHandler listGamesHandler,
ILoadGameHandler loadGameHandler,
IMoveHandler moveHandler,
IValidator<CreateGameRequest> createGameRequestValidator,
IValidator<JoinByCodeRequest> joinByCodeRequestValidator,
IValidator<JoinGameRequest> joinGameRequestValidator,
IValidator<ListGamesRequest> listGamesRequestValidator,
IValidator<LoadGameRequest> loadGameRequestValidator,
IValidator<MoveRequest> moveRequestValidator
) : base()
{
this.logger = logger;
this.communicationManager = communicationManager;
this.tokenManager = tokenManager;
this.createGameHandler = createGameHandler;
this.joinByCodeHandler = joinByCodeHandler;
this.joinGameHandler = joinGameHandler;
this.listGamesHandler = listGamesHandler;
this.loadGameHandler = loadGameHandler;
this.moveHandler = moveHandler;
this.createGameRequestValidator = createGameRequestValidator;
this.joinByCodeRequestValidator = joinByCodeRequestValidator;
this.joinGameRequestValidator = joinGameRequestValidator;
this.listGamesRequestValidator = listGamesRequestValidator;
this.loadGameRequestValidator = loadGameRequestValidator;
this.moveRequestValidator = moveRequestValidator;
}
public async Task HandleSocketRequest(HttpContext context)
{
var hasToken = context.Request.Query.Keys.Contains("token");
if (hasToken)
{
var oneTimeToken = context.Request.Query["token"][0];
var tokenAsGuid = Guid.Parse(oneTimeToken);
var userName = tokenManager.GetUsername(tokenAsGuid);
if (userName != null)
{
var socket = await context.WebSockets.AcceptWebSocketAsync();
communicationManager.SubscribeToBroadcast(socket, userName);
while (socket.State == WebSocketState.Open)
{
try
{
var message = await socket.ReceiveTextAsync();
if (string.IsNullOrWhiteSpace(message)) continue;
logger.LogInformation("Request \n{0}\n", message);
var request = JsonConvert.DeserializeObject<Request>(message);
if (!Enum.IsDefined(typeof(ClientAction), request.Action))
{
await socket.SendTextAsync("Error: Action not recognized.");
continue;
}
switch (request.Action)
{
case ClientAction.ListGames:
{
var req = JsonConvert.DeserializeObject<ListGamesRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req))
{
await listGamesHandler.Handle(req, userName);
}
break;
}
case ClientAction.CreateGame:
{
var req = JsonConvert.DeserializeObject<CreateGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req))
{
await createGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.JoinGame:
{
var req = JsonConvert.DeserializeObject<JoinGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req))
{
await joinGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.JoinByCode:
{
var req = JsonConvert.DeserializeObject<JoinByCodeRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req))
{
await joinByCodeHandler.Handle(req, userName);
}
break;
}
case ClientAction.LoadGame:
{
var req = JsonConvert.DeserializeObject<LoadGameRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req))
{
await loadGameHandler.Handle(req, userName);
}
break;
}
case ClientAction.Move:
{
var req = JsonConvert.DeserializeObject<MoveRequest>(message);
if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req))
{
await moveHandler.Handle(req, userName);
}
break;
}
}
}
catch (OperationCanceledException ex)
{
logger.LogError(ex.Message);
}
catch (WebSocketException ex)
{
logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}.");
logger.LogInformation("Probably tried writing to a closed socket.");
logger.LogError(ex.Message);
}
}
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
return;
}
}
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
public async Task<bool> ValidateRequestAndReplyIfInvalid<TRequest>(WebSocket socket, IValidator<TRequest> validator, TRequest request)
{
var results = validator.Validate(request);
if (!results.IsValid)
{
await socket.SendTextAsync(string.Join('\n', results.Errors.Select(_ => _.ErrorMessage).ToString()));
}
return results.IsValid;
}
}
}

View File

@@ -1,8 +1,11 @@
using FluentValidation;
using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.Services;
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@@ -39,11 +42,19 @@ namespace Gameboard.ShogiUI.Sockets
services.AddSingleton<IMoveHandler, MoveHandler>(); services.AddSingleton<IMoveHandler, MoveHandler>();
// Managers // Managers
services.AddSingleton<ISocketCommunicationManager, SocketCommunicationManager>();
services.AddSingleton<ISocketTokenManager, SocketTokenManager>();
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>(); services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
services.AddSingleton<IGameboardRepositoryManager, GameboardRepositoryManager>(); services.AddSingleton<ISocketTokenManager, SocketTokenManager>();
services.AddSingleton<IBoardManager, BoardManager>(); services.AddSingleton<IGameboardManager, GameboardManager>();
services.AddSingleton<IActiveSessionManager, ActiveSessionManager>();
// Services
services.AddSingleton<IValidator<CreateGameRequest>, CreateGameRequestValidator>();
services.AddSingleton<IValidator<JoinByCodeRequest>, JoinByCodeRequestValidator>();
services.AddSingleton<IValidator<JoinGameRequest>, JoinGameRequestValidator>();
services.AddSingleton<IValidator<ListGamesRequest>, ListGamesRequestValidator>();
services.AddSingleton<IValidator<LoadGameRequest>, LoadGameRequestValidator>();
services.AddSingleton<IValidator<MoveRequest>, MoveRequestValidator>();
services.AddSingleton<ISocketService, SocketService>();
// Repositories // Repositories
services.AddHttpClient("couchdb", c => services.AddHttpClient("couchdb", c =>
@@ -77,7 +88,7 @@ namespace Gameboard.ShogiUI.Sockets
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketConnectionManager socketConnectionManager) public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager)
{ {
var origins = new[] { var origins = new[] {
"http://localhost:3000", "https://localhost:3000", "http://localhost:3000", "https://localhost:3000",
@@ -135,10 +146,13 @@ namespace Gameboard.ShogiUI.Sockets
Formatting = Formatting.Indented, Formatting = Formatting.Indented,
ContractResolver = new DefaultContractResolver ContractResolver = new DefaultContractResolver
{ {
NamingStrategy = new CamelCaseNamingStrategy(), NamingStrategy = new CamelCaseNamingStrategy
{
ProcessDictionaryKeys = true
}
}, },
Converters = new[] { new StringEnumConverter() }, Converters = new[] { new StringEnumConverter() },
NullValueHandling = NullValueHandling.Ignore NullValueHandling = NullValueHandling.Ignore,
}; };
} }
} }

View File

@@ -5,10 +5,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" /> <PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" /> <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" /> <PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
<PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,92 @@
using AutoFixture;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PathFinding;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.UnitTests.PathFinding
{
[TestClass]
public class PlanarCollectionShould
{
private class SimpleElement : IPlanarElement
{
public static int Seed { get; private set; }
public MoveSet MoveSet => null;
public bool IsUpsideDown => false;
public SimpleElement()
{
Seed = Seed++;
}
}
private Fixture fixture;
[TestInitialize]
public void TestInitialize()
{
fixture = new Fixture();
}
[TestMethod]
public void Index()
{
// Arrange
var collection = new PlanarCollection<SimpleElement>(10, 10);
var expected1 = new SimpleElement();
var expected2 = new SimpleElement();
// Act
collection[0, 0] = expected1;
collection[2, 1] = expected2;
// Assert
collection[0, 0].Should().Be(expected1);
collection[2, 1].Should().Be(expected2);
}
[TestMethod]
public void Iterate()
{
// Arrange
var expected = new List<SimpleElement>();
for (var i = 0; i < 9; i++) expected.Add(new SimpleElement());
var collection = new PlanarCollection<SimpleElement>(3, 3);
for (var x = 0; x < 3; x++)
for (var y = 0; y < 3; y++)
collection[x, y] = expected[x + y];
// Act
var actual = new List<SimpleElement>();
foreach (var elem in collection)
actual.Add(elem);
// Assert
actual.Should().BeEquivalentTo(expected);
}
[TestMethod]
public void Yep()
{
var collection = new PlanarCollection<SimpleElement>(3, 3);
collection[0, 0] = new SimpleElement();
collection[1, 0] = new SimpleElement();
collection[0, 1] = new SimpleElement();
// Act
var array2d = new SimpleElement[3, 3];
for (var x = 0; x < 3; x++)
for (var y = 0; y < 3; y++)
{
array2d[x, y] = collection[x, y];
}
Console.WriteLine("hey");
}
}
}

View File

@@ -1,11 +1,10 @@
using FluentAssertions; using FluentAssertions;
using Gameboard.ShogiUI.Rules; using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Rules.Pieces;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using WhichPlayer = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPlayer;
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types.WhichPiece;
namespace Gameboard.ShogiUI.UnitTests.Rules namespace Gameboard.ShogiUI.UnitTests.Rules
{ {
[TestClass] [TestClass]
@@ -15,54 +14,54 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
public void InitializeBoardState() public void InitializeBoardState()
{ {
// Assert // Assert
var board = new ShogiBoard().Board; var board = new Shogi().Board;
// Assert pieces do not start promoted. // Assert pieces do not start promoted.
foreach (var piece in board) piece?.IsPromoted.Should().BeFalse(); foreach (var piece in board) piece?.IsPromoted.Should().BeFalse();
// Assert Player1. // Assert Player1.
for (var y = 0; y < 3; y++) for (var y = 0; y < 3; y++)
for (var x = 0; x < 9; x++) for (var x = 0; x < 9; x++)
board[x, y]?.Owner.Should().Be(WhichPlayer.Player2); board[y, x]?.Owner.Should().Be(WhichPlayer.Player2);
board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance); board[0, 0].WhichPiece.Should().Be(WhichPiece.Lance);
board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight);
board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[0, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[0, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[4, 0].WhichPiece.Should().Be(WhichPiece.King); board[0, 4].WhichPiece.Should().Be(WhichPiece.King);
board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[0, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[0, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[0, 7].WhichPiece.Should().Be(WhichPiece.Knight);
board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance);
board[0, 1].Should().BeNull(); board[1, 0].Should().BeNull();
board[1, 1].WhichPiece.Should().Be(WhichPiece.Rook); board[1, 1].WhichPiece.Should().Be(WhichPiece.Rook);
for (var x = 2; x < 7; x++) board[x, 1].Should().BeNull(); for (var x = 2; x < 7; x++) board[1, x].Should().BeNull();
board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop);
board[8, 1].Should().BeNull(); board[1, 8].Should().BeNull();
for (var x = 0; x < 9; x++) board[x, 2].WhichPiece.Should().Be(WhichPiece.Pawn); for (var x = 0; x < 9; x++) board[2, x].WhichPiece.Should().Be(WhichPiece.Pawn);
// Assert empty locations. // Assert empty locations.
for (var y = 3; y < 6; y++) for (var y = 3; y < 6; y++)
for (var x = 0; x < 9; x++) for (var x = 0; x < 9; x++)
board[x, y].Should().BeNull(); board[y, x].Should().BeNull();
// Assert Player2. // Assert Player2.
for (var y = 6; y < 9; y++) for (var y = 6; y < 9; y++)
for (var x = 0; x < 9; x++) for (var x = 0; x < 9; x++)
board[x, y]?.Owner.Should().Be(WhichPlayer.Player1); board[y, x]?.Owner.Should().Be(WhichPlayer.Player1);
board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance);
board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 1].WhichPiece.Should().Be(WhichPiece.Knight);
board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[8, 2].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[8, 3].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[4, 8].WhichPiece.Should().Be(WhichPiece.King); board[8, 4].WhichPiece.Should().Be(WhichPiece.King);
board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral); board[8, 5].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[8, 6].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[8, 7].WhichPiece.Should().Be(WhichPiece.Knight);
board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance);
board[0, 7].Should().BeNull(); board[7, 0].Should().BeNull();
board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop); board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop);
for (var x = 2; x < 7; x++) board[x, 7].Should().BeNull(); for (var x = 2; x < 7; x++) board[7, x].Should().BeNull();
board[7, 7].WhichPiece.Should().Be(WhichPiece.Rook); board[7, 7].WhichPiece.Should().Be(WhichPiece.Rook);
board[8, 7].Should().BeNull(); board[7, 8].Should().BeNull();
for (var x = 0; x < 9; x++) board[x, 6].WhichPiece.Should().Be(WhichPiece.Pawn); for (var x = 0; x < 9; x++) board[6, x].WhichPiece.Should().Be(WhichPiece.Pawn);
} }
[TestMethod] [TestMethod]
@@ -70,60 +69,52 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
{ {
var moves = new[] var moves = new[]
{ {
new Move
{
// Pawn // Pawn
From = new Vector2(0, 6), new Move(new Vector2(0, 6), new Vector2(0, 5))
To = new Vector2(0, 5)
}
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
shogi.Board[0, 6].Should().BeNull(); shogi.Board[6, 0].Should().BeNull();
shogi.Board[0, 5].WhichPiece.Should().Be(WhichPiece.Pawn); shogi.Board[5, 0].WhichPiece.Should().Be(WhichPiece.Pawn);
} }
[TestMethod] [TestMethod]
public void PreventInvalidMoves_MoveFromEmptyPosition() public void PreventInvalidMoves_MoveFromEmptyPosition()
{ {
// Arrange // Arrange
var shogi = new ShogiBoard(); var shogi = new Shogi();
// Prerequisit // Prerequisit
shogi.Board[4, 4].Should().BeNull(); shogi.Board[4, 4].Should().BeNull();
// Act // Act
var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }); var moveSuccess = shogi.Move(new Move(new Vector2(4, 4), new Vector2(4, 5)));
// Assert // Assert
moveSuccess.Should().BeFalse(); moveSuccess.Should().BeFalse();
shogi.Board[4, 4].Should().BeNull(); shogi.Board[4, 4].Should().BeNull();
shogi.Board[4, 5].Should().BeNull(); shogi.Board[5, 4].Should().BeNull();
} }
[TestMethod] [TestMethod]
public void PreventInvalidMoves_MoveToCurrentPosition() public void PreventInvalidMoves_MoveToCurrentPosition()
{ {
// Arrange // Arrange
var shogi = new ShogiBoard(); var shogi = new Shogi();
// Act - P1 "moves" pawn to the position it already exists at. // Act - P1 "moves" pawn to the position it already exists at.
var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 6), To = new Vector2(0, 6) }); var moveSuccess = shogi.Move(new Move(new Vector2(0, 6), new Vector2(0, 6)));
// Assert // Assert
moveSuccess.Should().BeFalse(); moveSuccess.Should().BeFalse();
shogi.Board[0, 6].WhichPiece.Should().Be(WhichPiece.Pawn); shogi.Board[6, 0].WhichPiece.Should().Be(WhichPiece.Pawn);
} }
[TestMethod] [TestMethod]
public void PreventInvalidMoves_MoveSet() public void PreventInvalidMoves_MoveSet()
{ {
var invalidLanceMove = new Move // Bishop moving lateral
{ var invalidLanceMove = new Move(new Vector2(1, 1), new Vector2(2, 1));
// Bishop moving lateral
From = new Vector2(1, 1),
To = new Vector2(2, 1)
};
var shogi = new ShogiBoard(); var shogi = new Shogi();
var moveSuccess = shogi.Move(invalidLanceMove); var moveSuccess = shogi.Move(invalidLanceMove);
moveSuccess.Should().BeFalse(); moveSuccess.Should().BeFalse();
@@ -135,30 +126,26 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
public void PreventInvalidMoves_Ownership() public void PreventInvalidMoves_Ownership()
{ {
// Arrange // Arrange
var shogi = new ShogiBoard(); var shogi = new Shogi();
shogi.WhoseTurn.Should().Be(WhichPlayer.Player1); shogi.WhoseTurn.Should().Be(WhichPlayer.Player1);
shogi.Board[8, 2].Owner.Should().Be(WhichPlayer.Player2); shogi.Board[2, 8].Owner.Should().Be(WhichPlayer.Player2);
// Act - Move Player2 Pawn when it's Player1 turn. // Act - Move Player2 Pawn when it's Player1 turn.
var moveSuccess = shogi.Move(new Move { From = new Vector2(8, 2), To = new Vector2(8, 3) }); var moveSuccess = shogi.Move(new Move(new Vector2(8, 2), new Vector2(8, 3)));
// Assert // Assert
moveSuccess.Should().BeFalse(); moveSuccess.Should().BeFalse();
shogi.Board[8, 6].WhichPiece.Should().Be(WhichPiece.Pawn); shogi.Board[6, 8].WhichPiece.Should().Be(WhichPiece.Pawn);
shogi.Board[8, 5].Should().BeNull(); shogi.Board[5, 8].Should().BeNull();
} }
[TestMethod] [TestMethod]
public void PreventInvalidMoves_MoveThroughAllies() public void PreventInvalidMoves_MoveThroughAllies()
{ {
var invalidLanceMove = new Move // Lance moving through the pawn before it.
{ var invalidLanceMove = new Move(new Vector2(0, 8), new Vector2(0, 4));
// Lance moving through the pawn before it.
From = new Vector2(0, 8),
To = new Vector2(0, 4)
};
var shogi = new ShogiBoard(); var shogi = new Shogi();
var moveSuccess = shogi.Move(invalidLanceMove); var moveSuccess = shogi.Move(invalidLanceMove);
moveSuccess.Should().BeFalse(); moveSuccess.Should().BeFalse();
@@ -169,20 +156,16 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
[TestMethod] [TestMethod]
public void PreventInvalidMoves_CaptureAlly() public void PreventInvalidMoves_CaptureAlly()
{ {
var invalidKnightMove = new Move // Knight capturing allied Pawn
{ var invalidKnightMove = new Move(new Vector2(1, 8), new Vector2(0, 6));
// Knight capturing allied Pawn
From = new Vector2(1, 8),
To = new Vector2(0, 6)
};
var shogi = new ShogiBoard(); var shogi = new Shogi();
var moveSuccess = shogi.Move(invalidKnightMove); var moveSuccess = shogi.Move(invalidKnightMove);
moveSuccess.Should().BeFalse(); moveSuccess.Should().BeFalse();
// Assert the Knight has not actually moved or captured. // Assert the Knight has not actually moved or captured.
shogi.Board[1, 0].WhichPiece.Should().Be(WhichPiece.Knight); shogi.Board[0, 1].WhichPiece.Should().Be(WhichPiece.Knight);
shogi.Board[0, 2].WhichPiece.Should().Be(WhichPiece.Pawn); shogi.Board[2, 0].WhichPiece.Should().Be(WhichPiece.Pawn);
} }
[TestMethod] [TestMethod]
@@ -192,25 +175,25 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var moves = new[] var moves = new[]
{ {
// P1 Pawn // P1 Pawn
new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, new Move(new Vector2(2, 6), new Vector2(2, 5)),
// P2 Pawn // P2 Pawn
new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, new Move(new Vector2(6, 2), new Vector2(6, 3)),
// P1 Bishop puts P2 in check // P1 Bishop puts P2 in check
new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) } new Move(new Vector2(1, 7), new Vector2(6, 2))
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
// Prerequisit // Prerequisit
shogi.InCheck.Should().Be(WhichPlayer.Player2); shogi.InCheck.Should().Be(WhichPlayer.Player2);
// Act - P2 moves Lance while remaining in check. // Act - P2 moves Lance while remaining in check.
var moveSuccess = shogi.Move(new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) }); var moveSuccess = shogi.Move(new Move(new Vector2(0, 8), new Vector2(0, 7)));
// Assert // Assert
moveSuccess.Should().BeFalse(); moveSuccess.Should().BeFalse();
shogi.InCheck.Should().Be(WhichPlayer.Player2); shogi.InCheck.Should().Be(WhichPlayer.Player2);
shogi.Board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); shogi.Board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance);
shogi.Board[8, 7].Should().BeNull(); shogi.Board[7, 8].Should().BeNull();
} }
[TestMethod] [TestMethod]
@@ -220,31 +203,31 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var moves = new[] var moves = new[]
{ {
// P1 Pawn // P1 Pawn
new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, new Move(new Vector2(2, 6), new Vector2(2, 5) ),
// P2 Pawn // P2 Pawn
new Move { From = new Vector2(0, 2), To = new Vector2(0, 3) }, new Move(new Vector2(0, 2), new Vector2(0, 3) ),
// P1 Bishop takes P2 Pawn // P1 Bishop takes P2 Pawn
new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, new Move(new Vector2(1, 7), new Vector2(6, 2) ),
// P2 Gold, block check from P1 Bishop. // P2 Gold, block check from P1 Bishop.
new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, new Move(new Vector2(5, 0), new Vector2(5, 1) ),
// P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance
new Move { From = new Vector2(6, 2), To = new Vector2(7, 1), IsPromotion = true }, new Move(new Vector2(6, 2), new Vector2(7, 1), true ),
// P2 Pawn again // P2 Pawn again
new Move { From = new Vector2(0, 3), To = new Vector2(0, 4) }, new Move(new Vector2(0, 3), new Vector2(0, 4) ),
// P1 Bishop takes P2 Knight // P1 Bishop takes P2 Knight
new Move { From = new Vector2(7, 1), To = new Vector2(7, 0) }, new Move(new Vector2(7, 1), new Vector2(7, 0) ),
// P2 Pawn again // P2 Pawn again
new Move { From = new Vector2(0, 4), To = new Vector2(0, 5) }, new Move(new Vector2(0, 4), new Vector2(0, 5) ),
// P1 Bishop takes P2 Lance // P1 Bishop takes P2 Lance
new Move { From = new Vector2(7, 0), To = new Vector2(8, 0) }, new Move(new Vector2(7, 0), new Vector2(8, 0) ),
// P2 Lance (move to make room for attempted P1 Pawn placement) // P2 Lance (move to make room for attempted P1 Pawn placement)
new Move { From = new Vector2(0, 0), To = new Vector2(0, 1) }, new Move(new Vector2(0, 0), new Vector2(0, 1) ),
// P1 arbitrary move // P1 arbitrary move
new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, new Move(new Vector2(4, 8), new Vector2(4, 7) ),
// P2 Pawn again, takes P1 Pawn // P2 Pawn again, takes P1 Pawn
new Move { From = new Vector2(0, 5), To = new Vector2(0, 6) }, new Move(new Vector2(0, 5) , new Vector2(0, 6) ),
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
// Prerequisites // Prerequisites
shogi.Hands[WhichPlayer.Player1].Count.Should().Be(4); shogi.Hands[WhichPlayer.Player1].Count.Should().Be(4);
@@ -255,27 +238,27 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
// Act | Assert - It is P1 turn // Act | Assert - It is P1 turn
/// try illegally placing Knight from the hand. /// try illegally placing Knight from the hand.
shogi.Board[7, 0].Should().BeNull(); shogi.Board[0, 7].Should().BeNull();
var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 0) }); var dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 0)));
dropSuccess.Should().BeFalse(); dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[7, 0].Should().BeNull(); shogi.Board[0, 7].Should().BeNull();
dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = new Vector2(7, 1) }); dropSuccess = shogi.Move(new Move(WhichPiece.Knight, new Vector2(7, 1)));
dropSuccess.Should().BeFalse(); dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[7, 1].Should().BeNull(); shogi.Board[1, 7].Should().BeNull();
/// try illegally placing Pawn from the hand /// try illegally placing Pawn from the hand
dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Pawn, To = new Vector2(7, 0) }); dropSuccess = shogi.Move(new Move(WhichPiece.Pawn, new Vector2(7, 0)));
dropSuccess.Should().BeFalse(); dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
shogi.Board[7, 0].Should().BeNull(); shogi.Board[0, 7].Should().BeNull();
/// try illegally placing Lance from the hand /// try illegally placing Lance from the hand
dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(7, 0) }); dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(7, 0)));
dropSuccess.Should().BeFalse(); dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[7, 0].Should().BeNull(); shogi.Board[0, 7].Should().BeNull();
} }
[TestMethod] [TestMethod]
@@ -285,34 +268,34 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var moves = new[] var moves = new[]
{ {
// P1 Pawn // P1 Pawn
new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, new Move(new Vector2(2, 6), new Vector2(2, 5)),
// P2 Pawn // P2 Pawn
new Move { From = new Vector2(8, 2), To = new Vector2(8, 3) }, new Move(new Vector2(8, 2), new Vector2(8, 3)),
// P1 Bishop, check // P1 Bishop, check
new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, new Move(new Vector2(1, 7), new Vector2(6, 2)),
// P2 Gold, block check // P2 Gold, block check
new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, new Move(new Vector2(5, 0), new Vector2(5, 1)),
// P1 arbitrary move // P1 arbitrary move
new Move { From = new Vector2(0, 6), To = new Vector2(0, 5) }, new Move(new Vector2(0, 6), new Vector2(0, 5)),
// P2 Bishop // P2 Bishop
new Move { From = new Vector2(7, 1), To = new Vector2(8, 2) }, new Move(new Vector2(7, 1), new Vector2(8, 2)),
// P1 Bishop takes P2 Lance // P1 Bishop takes P2 Lance
new Move { From = new Vector2(6, 2), To = new Vector2(8, 0) }, new Move(new Vector2(6, 2), new Vector2(8, 0)),
// P2 Bishop // P2 Bishop
new Move { From = new Vector2(8, 2), To = new Vector2(7, 1) }, new Move(new Vector2(8, 2), new Vector2(7, 1)),
// P1 arbitrary move // P1 arbitrary move
new Move { From = new Vector2(0, 5), To = new Vector2(0, 4) }, new Move(new Vector2(0, 5), new Vector2(0, 4)),
// P2 Bishop, check // P2 Bishop, check
new Move { From = new Vector2(7, 1), To = new Vector2(2, 6) }, new Move(new Vector2(7, 1), new Vector2(2, 6)),
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
// Prerequisites // Prerequisites
shogi.InCheck.Should().Be(WhichPlayer.Player1); shogi.InCheck.Should().Be(WhichPlayer.Player1);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
// Act - P1 tries to place a Lance while in check. // Act - P1 tries to place a Lance while in check.
var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(4, 4) }); var dropSuccess = shogi.Move(new Move(WhichPiece.Lance, new Vector2(4, 4)));
// Assert // Assert
dropSuccess.Should().BeFalse(); dropSuccess.Should().BeFalse();
@@ -328,31 +311,31 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var moves = new[] var moves = new[]
{ {
// P1 Pawn // P1 Pawn
new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, new Move(new Vector2(2, 6), new Vector2(2, 5)),
// P2 Pawn // P2 Pawn
new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, new Move(new Vector2(6, 2), new Vector2(6, 3)),
// P1 Bishop, capture P2 Pawn, check // P1 Bishop, capture P2 Pawn, check
new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }, new Move(new Vector2(1, 7), new Vector2(6, 2)),
// P2 Gold, block check // P2 Gold, block check
new Move { From = new Vector2(5, 0), To = new Vector2(5, 1) }, new Move(new Vector2(5, 0), new Vector2(5, 1)),
// P1 Bishop capture P2 Bishop // P1 Bishop capture P2 Bishop
new Move { From = new Vector2(6, 2), To = new Vector2(7, 1) }, new Move(new Vector2(6, 2), new Vector2(7, 1)),
// P2 arbitrary move // P2 arbitrary move
new Move { From = new Vector2(0, 0), To = new Vector2(0, 1) }, new Move(new Vector2(0, 0), new Vector2(0, 1)),
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
// Prerequisites // Prerequisites
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.Board[4, 0].Should().NotBeNull(); shogi.Board[0, 4].Should().NotBeNull();
// Act - P1 tries to place Bishop from hand to an already-occupied position // Act - P1 tries to place Bishop from hand to an already-occupied position
var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Bishop, To = new Vector2(4, 0) }); var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, new Vector2(4, 0)));
// Assert // Assert
dropSuccess.Should().BeFalse(); dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.Board[4, 0].WhichPiece.Should().Be(WhichPiece.King); shogi.Board[0, 4].WhichPiece.Should().Be(WhichPiece.King);
} }
[TestMethod] [TestMethod]
@@ -362,14 +345,14 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var moves = new[] var moves = new[]
{ {
// P1 Pawn // P1 Pawn
new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, new Move(new Vector2(2, 6), new Vector2(2, 5) ),
// P2 Pawn // P2 Pawn
new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) }, new Move(new Vector2(6, 2), new Vector2(6, 3) ),
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
// Act - P1 Bishop, check // Act - P1 Bishop, check
shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(6, 2) }); shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2)));
// Assert // Assert
shogi.InCheck.Should().Be(WhichPlayer.Player2); shogi.InCheck.Should().Be(WhichPlayer.Player2);
@@ -382,14 +365,14 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var moves = new[] var moves = new[]
{ {
// P1 Pawn // P1 Pawn
new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, new Move(new Vector2(2, 6), new Vector2(2, 5)),
// P2 Pawn // P2 Pawn
new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) } new Move(new Vector2(6, 2), new Vector2(6, 3))
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
// Act - P1 Bishop captures P2 Bishop // Act - P1 Bishop captures P2 Bishop
var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(7, 1) }); var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(7, 1)));
// Assert // Assert
moveSuccess.Should().BeTrue(); moveSuccess.Should().BeTrue();
@@ -398,20 +381,20 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
.Count(piece => piece?.WhichPiece == WhichPiece.Bishop) .Count(piece => piece?.WhichPiece == WhichPiece.Bishop)
.Should() .Should()
.Be(1); .Be(1);
shogi.Board[1, 7].Should().BeNull(); shogi.Board[7, 1].Should().BeNull();
shogi.Board[7, 1].WhichPiece.Should().Be(WhichPiece.Bishop); shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.Bishop);
shogi.Hands[WhichPlayer.Player1] shogi.Hands[WhichPlayer.Player1]
.Should() .Should()
.ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1); .ContainSingle(piece => piece.WhichPiece == WhichPiece.Bishop && piece.Owner == WhichPlayer.Player1);
// Act - P2 Silver captures P1 Bishop // Act - P2 Silver captures P1 Bishop
moveSuccess = shogi.Move(new Move { From = new Vector2(6, 0), To = new Vector2(7, 1) }); moveSuccess = shogi.Move(new Move(new Vector2(6, 0), new Vector2(7, 1)));
// Assert // Assert
moveSuccess.Should().BeTrue(); moveSuccess.Should().BeTrue();
shogi.Board[6, 0].Should().BeNull(); shogi.Board[0, 6].Should().BeNull();
shogi.Board[7, 1].WhichPiece.Should().Be(WhichPiece.SilverGeneral); shogi.Board[1, 7].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
shogi.Board shogi.Board
.Cast<Piece>() .Cast<Piece>()
.Count(piece => piece?.WhichPiece == WhichPiece.Bishop) .Count(piece => piece?.WhichPiece == WhichPiece.Bishop)
@@ -428,19 +411,19 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var moves = new[] var moves = new[]
{ {
// P1 Pawn // P1 Pawn
new Move { From = new Vector2(2, 6), To = new Vector2(2, 5) }, new Move(new Vector2(2, 6), new Vector2(2, 5) ),
// P2 Pawn // P2 Pawn
new Move { From = new Vector2(6, 2), To = new Vector2(6, 3) } new Move(new Vector2(6, 2), new Vector2(6, 3) )
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
// Act - P1 moves across promote threshold. // Act - P1 moves across promote threshold.
var moveSuccess = shogi.Move(new Move { From = new Vector2(1, 7), To = new Vector2(6, 2), IsPromotion = true }); var moveSuccess = shogi.Move(new Move(new Vector2(1, 7), new Vector2(6, 2), true));
// Assert // Assert
moveSuccess.Should().BeTrue(); moveSuccess.Should().BeTrue();
shogi.Board[1, 7].Should().BeNull(); shogi.Board[7, 1].Should().BeNull();
shogi.Board[6, 2].Should().Match<Piece>(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true); shogi.Board[2, 6].Should().Match<Piece>(piece => piece.WhichPiece == WhichPiece.Bishop && piece.IsPromoted == true);
} }
[TestMethod] [TestMethod]
@@ -450,30 +433,30 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var moves = new[] var moves = new[]
{ {
// P1 Rook // P1 Rook
new Move { From = new Vector2(7, 7), To = new Vector2(4, 7) }, new Move(new Vector2(7, 7), new Vector2(4, 7) ),
// P2 Gold // P2 Gold
new Move { From = new Vector2(3, 0), To = new Vector2(2, 1) }, new Move(new Vector2(3, 0), new Vector2(2, 1) ),
// P1 Pawn // P1 Pawn
new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, new Move(new Vector2(4, 6), new Vector2(4, 5) ),
// P2 other Gold // P2 other Gold
new Move { From = new Vector2(5, 0), To = new Vector2(6, 1) }, new Move(new Vector2(5, 0), new Vector2(6, 1) ),
// P1 same Pawn // P1 same Pawn
new Move { From = new Vector2(4, 5), To = new Vector2(4, 4) }, new Move(new Vector2(4, 5), new Vector2(4, 4) ),
// P2 Pawn // P2 Pawn
new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, new Move(new Vector2(4, 2), new Vector2(4, 3) ),
// P1 Pawn takes P2 Pawn // P1 Pawn takes P2 Pawn
new Move { From = new Vector2(4, 4), To = new Vector2(4, 3) }, new Move(new Vector2(4, 4), new Vector2(4, 3) ),
// P2 King // P2 King
new Move { From = new Vector2(4, 0), To = new Vector2(4, 1) }, new Move(new Vector2(4, 0), new Vector2(4, 1) ),
// P1 Pawn promotes, threatens P2 King // P1 Pawn promotes, threatens P2 King
new Move { From = new Vector2(4, 3), To = new Vector2(4, 2), IsPromotion = true }, new Move(new Vector2(4, 3), new Vector2(4, 2), true ),
// P2 King retreat // P2 King retreat
new Move { From = new Vector2(4, 1), To = new Vector2(4, 0) }, new Move(new Vector2(4, 1), new Vector2(4, 0) ),
}; };
var shogi = new ShogiBoard(moves); var shogi = new Shogi(moves);
// Act - P1 Pawn wins by checkmate. // Act - P1 Pawn wins by checkmate.
var moveSuccess = shogi.Move(new Move { From = new Vector2(4, 2), To = new Vector2(4, 1) }); var moveSuccess = shogi.Move(new Move(new Vector2(4, 2), new Vector2(4, 1)));
// Assert - checkmate // Assert - checkmate
moveSuccess.Should().BeTrue(); moveSuccess.Should().BeTrue();

View File

@@ -1,27 +0,0 @@
using FluentAssertions;
using Gameboard.ShogiUI.Sockets.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Gameboard.ShogiUI.UnitTests.Sockets
{
[TestClass]
public class CoordsModelShould
{
[TestMethod]
public void ConvertToNotation()
{
var letters = "ABCDEFGHI";
for (var x = 0; x < 8; x++) // file
{
for (var y = 0; y < 8; y++) // rank
{
var move = new Coords(x, y);
var actual = move.ToBoardNotation();
var expected = $"{letters[x]}{y + 1}";
actual.Should().Be(expected);
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,76 @@
using AutoFixture;
using FluentAssertions;
using FluentAssertions.Execution;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
using Xunit;
namespace Gameboard.ShogiUI.xUnitTests.RequestValidators
{
public class MoveRequestValidatorShould
{
private readonly Fixture fixture;
private readonly MoveRequestValidator validator;
public MoveRequestValidatorShould()
{
fixture = new Fixture();
validator = new MoveRequestValidator();
}
[Fact]
public void PreventInvalidPropertyCombinations()
{
// Arrange
var request = fixture.Create<MoveRequest>();
// Act
var results = validator.Validate(request);
// Assert
using (new AssertionScope())
{
results.IsValid.Should().BeFalse();
}
}
[Fact]
public void AllowValidPropertyCombinations()
{
// Arrange
var requestWithoutFrom = new MoveRequest()
{
Action = ClientAction.Move,
GameName = "Some game name",
Move = new Move()
{
IsPromotion = false,
PieceFromCaptured = WhichPiece.Bishop,
To = "A4"
}
};
var requestWithoutPieceFromCaptured = new MoveRequest()
{
Action = ClientAction.Move,
GameName = "Some game name",
Move = new Move()
{
From = "A1",
IsPromotion = false,
To = "A4"
}
};
// Act
var results = validator.Validate(requestWithoutFrom);
var results2 = validator.Validate(requestWithoutPieceFromCaptured);
// Assert
using (new AssertionScope())
{
results.IsValid.Should().BeTrue();
results2.IsValid.Should().BeTrue();
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@@ -34,7 +35,7 @@ namespace PathFinding
{ {
return false; return false;
} }
var element = collection[origin.X, origin.Y]; var element = collection[origin.Y, origin.X];
if (element == null) return false; if (element == null) return false;
var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination); var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination);
@@ -49,7 +50,7 @@ namespace PathFinding
while (shouldPath && next != destination) while (shouldPath && next != destination)
{ {
next = Vector2.Add(next, path.Direction); next = Vector2.Add(next, path.Direction);
var collider = collection[(int)next.X, (int)next.Y]; var collider = collection[(int)next.Y, (int)next.X];
if (collider != null) if (collider != null)
{ {
callback?.Invoke(collider, next); callback?.Invoke(collider, next);
@@ -65,14 +66,19 @@ namespace PathFinding
public void PathEvery(Vector2 from, Callback callback) public void PathEvery(Vector2 from, Callback callback)
{ {
var element = collection[from.X, from.Y]; var element = collection[from.Y, from.X];
if (element == null)
{
Console.WriteLine("Null element in PathEvery");
return;
}
foreach (var path in element.MoveSet.GetMoves()) foreach (var path in element.MoveSet.GetMoves())
{ {
var shouldPath = true; var shouldPath = true;
var next = Vector2.Add(from, path.Direction); ; var next = Vector2.Add(from, path.Direction); ;
while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0) while (shouldPath && next.X < width && next.Y < height && next.X >= 0 && next.Y >= 0)
{ {
var collider = collection[(int)next.X, (int)next.Y]; var collider = collection[(int)next.Y, (int)next.X];
if (collider != null) if (collider != null)
{ {
callback(collider, next); callback(collider, next);
@@ -97,7 +103,7 @@ namespace PathFinding
var next = Vector2.Add(origin, direction); var next = Vector2.Add(origin, direction);
while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height) while (next.X >= 0 && next.X < width && next.Y >= 0 && next.Y < height)
{ {
var element = collection[next.X, next.Y]; var element = collection[next.Y, next.X];
if (element != null) callback(element, next); if (element != null) callback(element, next);
next = Vector2.Add(next, direction); next = Vector2.Add(next, direction);
} }

View File

@@ -1,10 +1,10 @@
using PathFinding; using System;
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
namespace Gameboard.ShogiUI.Rules namespace PathFinding
{ {
// TODO: Get rid of this thing in favor of T[,] multi-dimensional array with extension methods.
public class PlanarCollection<T> : IPlanarCollection<T>, IEnumerable<T> where T : IPlanarElement public class PlanarCollection<T> : IPlanarCollection<T>, IEnumerable<T> where T : IPlanarElement
{ {
public delegate void ForEachDelegate(T element, int x, int y); public delegate void ForEachDelegate(T element, int x, int y);
@@ -19,12 +19,12 @@ namespace Gameboard.ShogiUI.Rules
array = new T[width * height]; array = new T[width * height];
} }
public T? this[int x, int y] public T? this[int y, int x]
{ {
get => array[y * width + x]; get => array[y * width + x];
set => array[y * width + x] = value; set => array[y * width + x] = value;
} }
public T? this[float x, float y] public T? this[float y, float x]
{ {
get => array[(int)y * width + (int)x]; get => array[(int)y * width + (int)x];
set => array[(int)y * width + (int)x] = value; set => array[(int)y * width + (int)x] = value;
@@ -32,8 +32,8 @@ namespace Gameboard.ShogiUI.Rules
public int GetLength(int dimension) => dimension switch public int GetLength(int dimension) => dimension switch
{ {
0 => width, 0 => height,
1 => height, 1 => width,
_ => throw new IndexOutOfRangeException() _ => throw new IndexOutOfRangeException()
}; };
@@ -43,15 +43,17 @@ namespace Gameboard.ShogiUI.Rules
{ {
for (var y = 0; y < height; y++) for (var y = 0; y < height; y++)
{ {
if (this[x, y] != null) var elem = this[y, x];
callback(this[x, y], x, y); if (elem != null)
callback(elem, x, y);
} }
} }
} }
public IEnumerator<T> GetEnumerator() public IEnumerator<T> GetEnumerator()
{ {
foreach (var item in array) yield return item; foreach (var item in array)
if (item != null) yield return item;
} }
IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => array.GetEnumerator();