before deleting Rules

This commit is contained in:
2021-05-08 10:26:04 -05:00
parent 05a9c71499
commit f8f779e84c
80 changed files with 1109 additions and 832 deletions

View File

@@ -11,7 +11,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.Rules.csproj" /> <ProjectReference Include="..\Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -16,33 +16,33 @@ namespace Benchmarking
public Benchmarks() public Benchmarks()
{ {
moves = new[] //moves = new[]
{ //{
// P1 Rook // // P1 Rook
new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) }, // new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) },
// P2 Gold // // P2 Gold
new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) }, // new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) },
// P1 Pawn // // P1 Pawn
new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) }, // new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) },
// P2 other Gold // // P2 other Gold
new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) }, // new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) },
// P1 same Pawn // // P1 same Pawn
new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) }, // new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) },
// P2 Pawn // // P2 Pawn
new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) }, // new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) },
// P1 Pawn takes P2 Pawn // // P1 Pawn takes P2 Pawn
new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) }, // new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) },
// P2 King // // P2 King
new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) }, // new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) },
// P1 Pawn promotes // // P1 Pawn promotes
new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true }, // new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true },
// P2 King retreat // // P2 King retreat
new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) }, // new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) },
}; //};
var rand = new Random(); //var rand = new Random();
directions = new Vector2[10]; //directions = new Vector2[10];
for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2)); //for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2));
} }
//[Benchmark] //[Benchmark]

View File

@@ -4,4 +4,8 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="Results\" />
</ItemGroup>
</Project> </Project>

16
CouchDB/CouchDocument.cs Normal file
View File

@@ -0,0 +1,16 @@
namespace CouchDB
{
public class CouchDocument<T>
{
public readonly string _id;
public readonly string type;
public readonly T model;
public CouchDocument(string id, T model)
{
_id = id;
this.model = model;
type = nameof(T);
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
namespace CouchDB.Selectors
{
public class CouchQuery
{
public static CouchQuery Select => new();
private readonly List<Equals> equals;
protected CouchQuery()
{
equals = new List<Equals>();
}
public CouchQuery WithEqual(string key, string value)
{
equals.Add(new Equals(key, value));
return this;
}
public override string ToString()
{
var selector = string.Join(",", equals);
return $"{{ \"selector\": {selector}";
}
}
}

View File

@@ -0,0 +1,18 @@
namespace CouchDB.Selectors
{
public class Equals
{
private readonly string key;
private readonly string value;
internal Equals(string key, string value)
{
this.key = key;
this.value = value;
}
public override string ToString()
{
return $"{{ \"{key}\": {{ \"$eq\": {value}}} }}";
}
}
}

View File

@@ -1,14 +0,0 @@
using System.Diagnostics;
using System.Numerics;
namespace Gameboard.ShogiUI.Rules
{
[DebuggerDisplay("{From} - {To}")]
public class Move
{
public WhichPiece? PieceFromCaptured { get; set; }
public Vector2 From { get; set; }
public Vector2 To { get; set; }
public bool IsPromotion { get; set; }
}
}

View File

@@ -1,6 +0,0 @@
namespace Gameboard.ShogiUI.Domain
{
public class Board
{
}
}

View File

@@ -1,30 +0,0 @@
namespace Gameboard.ShogiUI.Domain
{
public class Match
{
public string Name { get; }
public string Player1 { get; }
public string Player2 { get; }
/// <summary>
/// Initialize pre-existing Match.
/// </summary>
public Match(MatchMeta meta, Board board)
{
Name = meta.Name;
Player1 = meta.Player1;
Player2 = meta.Player2;
}
/// <summary>
/// Create a new Match.
/// </summary>
public Match(string name, string player1)
{
Name = name;
Player1 = player1;
}
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Domain
{
public class MatchMeta
{
public string Name { get; }
public string Player1 { get; }
public string Player2 { get; }
}
}

View File

@@ -4,6 +4,7 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel> <AnalysisLevel>5</AnalysisLevel>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,27 @@
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

@@ -14,7 +14,7 @@ namespace Gameboard.ShogiUI.Rules.Pieces
new PathFinding.Move(Direction.Right), new PathFinding.Move(Direction.Right),
new PathFinding.Move(Direction.Down) new PathFinding.Move(Direction.Down)
}; };
public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldenGeneral, owner) public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldGeneral, owner)
{ {
moveSet = new MoveSet(this, Moves); moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, Moves); promotedMoveSet = new MoveSet(this, Moves);

View File

@@ -25,7 +25,7 @@ namespace Gameboard.ShogiUI.Rules.Pieces
public bool CanPromote => !IsPromoted public bool CanPromote => !IsPromoted
&& WhichPiece != WhichPiece.King && WhichPiece != WhichPiece.King
&& WhichPiece != WhichPiece.GoldenGeneral; && WhichPiece != WhichPiece.GoldGeneral;
public void ToggleOwnership() public void ToggleOwnership()
{ {

View File

@@ -8,7 +8,7 @@ namespace Gameboard.ShogiUI.Rules
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);
private readonly T[] array; private readonly T?[] array;
private readonly int width; private readonly int width;
private readonly int height; private readonly int height;
@@ -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 x, int y]
{ {
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 x, float y]
{ {
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;

View File

@@ -14,9 +14,8 @@ namespace Gameboard.ShogiUI.Rules
public class ShogiBoard public class ShogiBoard
{ {
private delegate void MoveSetCallback(Piece piece, Vector2 position); private delegate void MoveSetCallback(Piece piece, Vector2 position);
private readonly bool isValidationBoard;
private readonly PathFinder2D<Piece> pathFinder; private readonly PathFinder2D<Piece> pathFinder;
private ShogiBoard validationBoard; private ShogiBoard? 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; }
@@ -26,7 +25,6 @@ namespace Gameboard.ShogiUI.Rules
public WhichPlayer? InCheck { get; private set; } public WhichPlayer? InCheck { get; private set; }
public bool IsCheckmate { get; private set; } public bool IsCheckmate { get; private set; }
public string Error { get; private set; } public string Error { get; private set; }
public ShogiBoard() public ShogiBoard()
@@ -41,6 +39,7 @@ namespace Gameboard.ShogiUI.Rules
InitializeBoardState(); InitializeBoardState();
player1King = new Vector2(4, 8); player1King = new Vector2(4, 8);
player2King = new Vector2(4, 0); player2King = new Vector2(4, 0);
Error = string.Empty;
} }
public ShogiBoard(IList<Move> moves) : this() public ShogiBoard(IList<Move> moves) : this()
@@ -57,11 +56,16 @@ namespace Gameboard.ShogiUI.Rules
private ShogiBoard(ShogiBoard toCopy) private ShogiBoard(ShogiBoard toCopy)
{ {
isValidationBoard = true;
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++)
Board[x, y] = toCopy.Board[x, y]?.DeepClone(); {
var piece = toCopy.Board[x, y];
if (piece != null)
{
Board[x, y] = piece.DeepClone();
}
}
pathFinder = new PathFinder2D<Piece>(Board); pathFinder = new PathFinder2D<Piece>(Board);
MoveHistory = new List<Move>(toCopy.MoveHistory); MoveHistory = new List<Move>(toCopy.MoveHistory);
@@ -72,6 +76,7 @@ namespace Gameboard.ShogiUI.Rules
}; };
player1King = toCopy.player1King; player1King = toCopy.player1King;
player2King = toCopy.player2King; player2King = toCopy.player2King;
Error = toCopy.Error;
} }
public bool Move(Move move) public bool Move(Move move)
@@ -103,7 +108,7 @@ namespace Gameboard.ShogiUI.Rules
validationBoard = new ShogiBoard(this); validationBoard = new ShogiBoard(this);
} }
var isValid = move.PieceFromCaptured.HasValue var isValid = move.PieceFromHand.HasValue
? validationBoard.PlaceFromHand(move) ? validationBoard.PlaceFromHand(move)
: validationBoard.PlaceFromBoard(move); : validationBoard.PlaceFromBoard(move);
if (!isValid) if (!isValid)
@@ -123,20 +128,20 @@ namespace Gameboard.ShogiUI.Rules
} }
// The move is valid and legal; update board state. // The move is valid and legal; update board state.
if (move.PieceFromCaptured.HasValue) PlaceFromHand(move); if (move.PieceFromHand.HasValue) PlaceFromHand(move);
else PlaceFromBoard(move); else PlaceFromBoard(move);
return true; return true;
} }
/// <returns>True if the move was successful.</returns> /// <returns>True if the move was successful.</returns>
private bool PlaceFromHand(Move move) private bool PlaceFromHand(Move move)
{ {
if (move.PieceFromCaptured.HasValue == false) return false; //Invalid move if (move.PieceFromHand.HasValue == false) return false; //Invalid move
var index = Hands[WhoseTurn].FindIndex(p => p.WhichPiece == move.PieceFromCaptured); 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.X, move.To.Y] != null) return false; // Invalid move; cannot capture while playing from the hand.
var minimumY = 0; var minimumY = 0;
switch (move.PieceFromCaptured.Value) switch (move.PieceFromHand.Value)
{ {
case WhichPiece.Knight: case WhichPiece.Knight:
// Knight cannot be placed onto the farthest two ranks from the hand. // Knight cannot be placed onto the farthest two ranks from the hand.
@@ -160,7 +165,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.X, move.From.Y]; var fromPiece = Board[move.From.Value.X, move.From.Value.Y];
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)}.";
@@ -171,7 +176,7 @@ namespace Gameboard.ShogiUI.Rules
Error = "Not allowed to move the opponents piece"; Error = "Not allowed to move the opponents piece";
return false; // Invalid move; cannot move other players pieces. return false; // Invalid move; cannot move other players pieces.
} }
if (IsPathable(move.From, move.To) == false) if (IsPathable(move.From.Value, move.To) == false)
{ {
Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set."; Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set.";
return false; // Invalid move; move not part of move-set. return false; // Invalid move; move not part of move-set.
@@ -188,17 +193,17 @@ namespace Gameboard.ShogiUI.Rules
//Mutate the board. //Mutate the board.
if (move.IsPromotion) if (move.IsPromotion)
{ {
if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Y < 3)) if (WhoseTurn == WhichPlayer.Player1 && (move.To.Y < 3 || move.From.Value.Y < 3))
{ {
fromPiece.Promote(); fromPiece.Promote();
} }
else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Y > 5)) else if (WhoseTurn == WhichPlayer.Player2 && (move.To.Y > 5 || move.From.Value.Y > 5))
{ {
fromPiece.Promote(); fromPiece.Promote();
} }
} }
Board[move.To.X, move.To.Y] = fromPiece; Board[move.To.X, move.To.Y] = fromPiece;
Board[move.From.X, move.From.Y] = null; Board[move.From.Value.X, move.From.Value.Y] = null;
if (fromPiece.WhichPiece == WhichPiece.King) if (fromPiece.WhichPiece == WhichPiece.King)
{ {
if (fromPiece.Owner == WhichPlayer.Player1) if (fromPiece.Owner == WhichPlayer.Player1)
@@ -239,7 +244,7 @@ namespace Gameboard.ShogiUI.Rules
if (pathFinder.PathTo(move.To, kingPosition)) return true; if (pathFinder.PathTo(move.To, kingPosition)) return true;
// Get line equation from king through the now-unoccupied location. // Get line equation from king through the now-unoccupied location.
var direction = Vector2.Subtract(kingPosition, move.From); var direction = Vector2.Subtract(kingPosition, move.From.Value);
var slope = Math.Abs(direction.Y / direction.X); var slope = Math.Abs(direction.Y / direction.X);
// If absolute slope is 45°, look for a bishop along the line. // If absolute slope is 45°, look for a bishop along the line.
// If absolute slope is 0° or 90°, look for a rook along the line. // If absolute slope is 0° or 90°, look for a rook along the line.
@@ -304,7 +309,7 @@ namespace Gameboard.ShogiUI.Rules
pathFinder.PathEvery(from, (other, position) => pathFinder.PathEvery(from, (other, position) =>
{ {
if (validationBoard == null) validationBoard = new ShogiBoard(this); if (validationBoard == null) validationBoard = new ShogiBoard(this);
var moveToTry = new Move { From = from, To = position }; var moveToTry = new Move(from, position, false);
var moveSuccess = validationBoard.TryMove(moveToTry); var moveSuccess = validationBoard.TryMove(moveToTry);
if (moveSuccess) if (moveSuccess)
{ {

View File

@@ -3,7 +3,7 @@
public enum WhichPiece public enum WhichPiece
{ {
King, King,
GoldenGeneral, GoldGeneral,
SilverGeneral, SilverGeneral,
Bishop, Bishop,
Rook, Rook,

View File

@@ -4,7 +4,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
{ {
public class GetGuestToken public class GetGuestToken
{ {
public string ClientId { get; set; } public string? ClientId { get; set; }
} }
public class GetGuestTokenResponse public class GetGuestTokenResponse

View File

@@ -0,0 +1,10 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
{
public class PostSession
{
public string Name { get; set; }
public string Player1 { get; set; }
public string Player2 { get; set; }
public bool IsPrivate { get; set; }
}
}

View File

@@ -4,6 +4,7 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel> <AnalysisLevel>5</AnalysisLevel>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -4,6 +4,6 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces
{ {
public interface IRequest public interface IRequest
{ {
ClientAction Action { get; set; } ClientAction Action { get; }
} }
} }

View File

@@ -6,13 +6,13 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public class CreateGameRequest : IRequest public class CreateGameRequest : IRequest
{ {
public ClientAction Action { get; set; } public ClientAction Action { get; set; }
public string GameName { get; set; } public string GameName { get; set; } = string.Empty;
public bool IsPrivate { get; set; } public bool IsPrivate { get; set; }
} }
public class CreateGameResponse : IResponse public class CreateGameResponse : IResponse
{ {
public string Action { get; private set; } public string Action { get; }
public string Error { get; set; } public string Error { get; set; }
public Game Game { get; set; } public Game Game { get; set; }
public string PlayerName { get; set; } public string PlayerName { get; set; }
@@ -20,6 +20,9 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public CreateGameResponse(ClientAction action) public CreateGameResponse(ClientAction action)
{ {
Action = action.ToString(); Action = action.ToString();
Error = string.Empty;
Game = new Game();
PlayerName = string.Empty;
} }
} }
} }

View File

@@ -1,16 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
{
public class ErrorResponse : IResponse
{
public string Action { get; private set; }
public string Error { get; set; }
public ErrorResponse(ClientAction action)
{
Action = action.ToString();
}
}
}

View File

@@ -1,11 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
{
public class JoinByCode : IRequest
{
public ClientAction Action { get; set; }
public string JoinCode { get; set; }
}
}

View File

@@ -3,6 +3,12 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
{ {
public class JoinByCodeRequest : IRequest
{
public ClientAction Action { 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; }
@@ -11,7 +17,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public class JoinGameResponse : IResponse public class JoinGameResponse : IResponse
{ {
public string Action { get; private set; } public string Action { get; }
public string Error { get; set; } public string Error { get; set; }
public string GameName { get; set; } public string GameName { get; set; }
public string PlayerName { get; set; } public string PlayerName { get; set; }

View File

@@ -11,7 +11,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public class ListGamesResponse : IResponse public class ListGamesResponse : IResponse
{ {
public string Action { get; private set; } public string Action { get; }
public string Error { get; set; } public string Error { get; set; }
public ICollection<Game> Games { get; set; } public ICollection<Game> Games { get; set; }

View File

@@ -1,6 +1,5 @@
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
{ {
@@ -12,7 +11,7 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public class LoadGameResponse : IResponse public class LoadGameResponse : IResponse
{ {
public string Action { get; private set; } public string Action { get; }
public Game Game { get; set; } public Game Game { get; set; }
public BoardState BoardState { get; set; } public BoardState BoardState { get; set; }
public string Error { get; set; } public string Error { get; set; }

View File

@@ -6,8 +6,8 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public class MoveRequest : IRequest public class MoveRequest : IRequest
{ {
public ClientAction Action { get; set; } public ClientAction Action { get; set; }
public string GameName { get; set; } public string GameName { get; set; } = string.Empty;
public Move Move { get; set; } public Move Move { get; set; } = new Move();
} }
public class MoveResponse : IResponse public class MoveResponse : IResponse
@@ -21,6 +21,10 @@ namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
public MoveResponse(ClientAction action) public MoveResponse(ClientAction action)
{ {
Action = action.ToString(); Action = action.ToString();
Error = string.Empty;
GameName = string.Empty;
BoardState = new BoardState();
PlayerName = string.Empty;
} }
} }
} }

View File

@@ -1,11 +1,12 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
{ {
public class BoardState public class BoardState
{ {
public Piece[,] Board { get; set; } public Piece[,] Board { get; set; } = new Piece[0, 0];
public IReadOnlyCollection<Piece> Player1Hand { get; set; } public IReadOnlyCollection<Piece> Player1Hand { get; set; } = Array.Empty<Piece>();
public IReadOnlyCollection<Piece> Player2Hand { get; set; } public IReadOnlyCollection<Piece> Player2Hand { get; set; } = Array.Empty<Piece>();
} }
} }

View File

@@ -7,7 +7,6 @@
JoinGame, JoinGame,
JoinByCode, JoinByCode,
LoadGame, LoadGame,
Move, Move
KeepAlive
} }
} }

View File

@@ -1,13 +1,14 @@
using System.Collections.Generic; using System;
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 GameName { get; set; } 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 guy
/// </summary> /// </summary>
public IReadOnlyList<string> Players { get; set; } public IReadOnlyList<string> Players { get; set; } = Array.Empty<string>();
} }
} }

View File

@@ -2,11 +2,11 @@
{ {
public class Move public class Move
{ {
public string PieceFromCaptured { get; set; } public WhichPiece? PieceFromCaptured { get; set; }
/// <summary>Board position notation, like A3 or G1</summary> /// <summary>Board position notation, like A3 or G1</summary>
public string From { get; set; } public string? From { get; set; }
/// <summary>Board position notation, like A3 or G1</summary> /// <summary>Board position notation, like A3 or G1</summary>
public string To { get; set; } public string To { get; set; } = string.Empty;
public bool IsPromotion { get; set; } public bool IsPromotion { get; set; }
} }
} }

View File

@@ -2,8 +2,8 @@
{ {
public class Piece public class Piece
{ {
public WhichPiece WhichPiece { get; set; }
public bool IsPromoted { get; set; } public bool IsPromoted { get; set; }
public WhichPiece WhichPiece { get; set; }
public WhichPlayer Owner { get; set; }
} }
} }

View File

@@ -0,0 +1,8 @@
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
{
public enum WhichPlayer
{
Player1,
Player2
}
}

View File

@@ -9,15 +9,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.S
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.Rules.csproj", "{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gameboard.ShogiUI.Domain", "Gameboard.ShogiUI.Domain\Gameboard.ShogiUI.Domain.csproj", "{2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CouchDB", "CouchDB\CouchDB.csproj", "{EDFED1DF-253D-463B-842A-0B66F95214A7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Rules", "Gameboard.ShogiUI.Rules\Gameboard.ShogiUI.Rules.csproj", "{D7130FAF-CEC4-4567-A9F0-22C060E9B508}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -33,10 +33,6 @@ Global
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU
{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5A7C4EF-549F-40A8-A0BD-DA2C7C0A6CF4}.Release|Any CPU.Build.0 = Release|Any CPU
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -49,10 +45,14 @@ Global
{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU
{2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDFED1DF-253D-463B-842A-0B66F95214A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2CB188B7-3EE8-44FB-9548-8C0CFBF7E40B}.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
{D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7130FAF-CEC4-4567-A9F0-22C060E9B508}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -1,4 +1,5 @@
using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; 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;
@@ -14,17 +15,20 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
public class GameController : ControllerBase public class GameController : ControllerBase
{ {
private readonly IGameboardRepositoryManager manager; private readonly IGameboardRepositoryManager manager;
private readonly ISocketCommunicationManager communicationManager;
private readonly IGameboardRepository repository; private readonly IGameboardRepository repository;
public GameController( public GameController(
IGameboardRepository repository, IGameboardRepository repository,
IGameboardRepositoryManager manager) IGameboardRepositoryManager manager,
ISocketCommunicationManager communicationManager)
{ {
this.manager = manager; this.manager = manager;
this.communicationManager = communicationManager;
this.repository = repository; this.repository = repository;
} }
[Route("JoinCode")] [HttpPost("JoinCode")]
public async Task<IActionResult> PostGameInvitation([FromBody] PostGameInvitation request) public async Task<IActionResult> PostGameInvitation([FromBody] PostGameInvitation request)
{ {
var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
@@ -41,7 +45,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
} }
[AllowAnonymous] [AllowAnonymous]
[Route("GuestJoinCode")] [HttpPost("GuestJoinCode")]
public async Task<IActionResult> PostGuestGameInvitation([FromBody] PostGuestGameInvitation request) public async Task<IActionResult> PostGuestGameInvitation([FromBody] PostGuestGameInvitation request)
{ {
@@ -57,5 +61,26 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
return new UnauthorizedResult(); return new UnauthorizedResult();
} }
} }
// TODO: Use JWT tokens for guests so they can authenticate and use API routes, too.
//[Route("")]
//public async Task<IActionResult> PostSession([FromBody] PostSession request)
//{
// var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2);
// var success = await repository.CreateSession(model);
// if (success)
// {
// var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Socket.Types.ClientAction.CreateGame)
// {
// Game = model.ToServiceModel(),
// PlayerName =
// }
// var task = request.IsPrivate
// ? communicationManager.BroadcastToPlayers(response, userName)
// : communicationManager.BroadcastToAll(response);
// return new CreatedResult("", null);
// }
// return new ConflictResult();
//}
} }
} }

View File

@@ -1,8 +1,10 @@
using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers; 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;
using Microsoft.Extensions.Logging;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -13,18 +15,24 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
[ApiController] [ApiController]
public class SocketController : ControllerBase public class SocketController : ControllerBase
{ {
private readonly ILogger<SocketController> logger;
private readonly ISocketTokenManager tokenManager; private readonly ISocketTokenManager tokenManager;
private readonly IGameboardRepositoryManager gameboardManager; private readonly IGameboardRepositoryManager gameboardManager;
private readonly IGameboardRepository gameboardRepository;
public SocketController( public SocketController(
ILogger<SocketController> logger,
ISocketTokenManager tokenManager, ISocketTokenManager tokenManager,
IGameboardRepositoryManager gameboardManager) IGameboardRepositoryManager gameboardManager,
IGameboardRepository gameboardRepository)
{ {
this.logger = logger;
this.tokenManager = tokenManager; this.tokenManager = tokenManager;
this.gameboardManager = gameboardManager; this.gameboardManager = gameboardManager;
this.gameboardRepository = gameboardRepository;
} }
[Route("Token")] [HttpGet("Token")]
public IActionResult GetToken() public IActionResult GetToken()
{ {
var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
@@ -33,7 +41,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
} }
[AllowAnonymous] [AllowAnonymous]
[Route("GuestToken")] [HttpGet("GuestToken")]
public async Task<IActionResult> GetGuestToken([FromQuery] GetGuestToken request) public async Task<IActionResult> GetGuestToken([FromQuery] GetGuestToken request)
{ {
if (request.ClientId == null) if (request.ClientId == null)
@@ -44,7 +52,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
} }
else else
{ {
if (await gameboardManager.PlayerExists(request.ClientId)) if (await gameboardRepository.IsGuestUser(request.ClientId))
{ {
var token = tokenManager.GenerateToken(request.ClientId); var token = tokenManager.GenerateToken(request.ClientId);
return new JsonResult(new GetGuestTokenResponse(request.ClientId, token)); return new JsonResult(new GetGuestTokenResponse(request.ClientId, token));

View File

@@ -4,10 +4,18 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel> <AnalysisLevel>5</AnalysisLevel>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Gameboard.Shogi.Api.ServiceModels" Version="2.13.0" /> <None Remove="Repositories\CouchModels\Readme.md" />
</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" />
@@ -17,7 +25,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.BoardState\Gameboard.ShogiUI.Rules.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" />
</ItemGroup> </ItemGroup>

View File

@@ -6,7 +6,7 @@ namespace Gameboard.ShogiUI.Sockets.Managers
public interface IBoardManager public interface IBoardManager
{ {
void Add(string sessionName, ShogiBoard board); void Add(string sessionName, ShogiBoard board);
ShogiBoard Get(string sessionName); ShogiBoard? Get(string sessionName);
} }
public class BoardManager : IBoardManager public class BoardManager : IBoardManager
@@ -20,10 +20,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers
public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board); public void Add(string sessionName, ShogiBoard board) => Boards.TryAdd(sessionName, board);
public ShogiBoard Get(string sessionName) public ShogiBoard? Get(string sessionName)
{ {
if (Boards.TryGetValue(sessionName, out var board)) if (Boards.TryGetValue(sessionName, out var board))
{
return board; return board;
}
return null; return null;
} }
} }

View File

@@ -1,61 +1,55 @@
using Gameboard.Shogi.Api.ServiceModels.Messages; using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{ {
public interface ICreateGameHandler
{
Task Handle(CreateGameRequest request, string userName);
}
// TODO: This doesn't need to be a socket action. // TODO: This doesn't need to be a socket action.
// 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 : IActionHandler public class CreateGameHandler : ICreateGameHandler
{ {
private readonly IGameboardRepository repository; private readonly IGameboardRepositoryManager manager;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketCommunicationManager communicationManager;
public CreateGameHandler( public CreateGameHandler(
ISocketCommunicationManager communicationManager, ISocketCommunicationManager communicationManager,
IGameboardRepository repository) IGameboardRepositoryManager manager)
{ {
this.repository = repository; this.manager = manager;
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
} }
public async Task Handle(string json, string userName) public async Task Handle(CreateGameRequest request, string userName)
{ {
var request = JsonConvert.DeserializeObject<CreateGameRequest>(json); var model = new Session(request.GameName, request.IsPrivate, userName);
var sessionName = await repository.PostSession(new PostSession var success = await manager.CreateSession(model);
if (!success)
{ {
SessionName = request.GameName, var error = new CreateGameResponse(request.Action)
PlayerName = userName, {
IsPrivate = request.IsPrivate Error = "Unable to create game with this name."
}); };
await communicationManager.BroadcastToPlayers(error, userName);
}
var response = new CreateGameResponse(request.Action) var response = new CreateGameResponse(request.Action)
{ {
PlayerName = userName, PlayerName = userName,
Game = new Game Game = model.ToServiceModel()
{
GameName = sessionName,
Players = new[] { userName }
}
}; };
if (string.IsNullOrWhiteSpace(sessionName)) var task = request.IsPrivate
{ ? communicationManager.BroadcastToPlayers(response, userName)
response.Error = "Game already exists."; : communicationManager.BroadcastToAll(response);
}
if (request.IsPrivate) await task;
{
await communicationManager.BroadcastToPlayers(response, userName);
}
else
{
await communicationManager.BroadcastToAll(response);
}
} }
} }
} }

View File

@@ -1,15 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{
public interface IActionHandler
{
/// <summary>
/// Responsible for parsing json and handling the request.
/// </summary>
Task Handle(string json, string userName);
}
public delegate IActionHandler ActionHandlerResolver(ClientAction action);
}

View File

@@ -1,13 +1,14 @@
using Gameboard.Shogi.Api.ServiceModels.Messages; 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 Newtonsoft.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{ {
public class JoinByCodeHandler : IActionHandler public interface IJoinByCodeHandler
{
Task Handle(JoinByCodeRequest request, string userName);
}
public class JoinByCodeHandler : IJoinByCodeHandler
{ {
private readonly IGameboardRepository repository; private readonly IGameboardRepository repository;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketCommunicationManager communicationManager;
@@ -20,44 +21,44 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
} }
public async Task Handle(string json, string userName) public async Task Handle(JoinByCodeRequest request, string userName)
{ {
var request = JsonConvert.DeserializeObject<JoinByCode>(json); //var request = JsonConvert.DeserializeObject<JoinByCode>(json);
var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession //var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession
{ //{
PlayerName = userName, // PlayerName = userName,
JoinCode = request.JoinCode // JoinCode = request.JoinCode
}); //});
if (sessionName == null) //if (sessionName == null)
{ //{
var response = new JoinGameResponse(ClientAction.JoinByCode) // var response = new JoinGameResponse(ClientAction.JoinByCode)
{ // {
PlayerName = userName, // PlayerName = userName,
GameName = sessionName, // GameName = sessionName,
Error = "Error joining game." // Error = "Error joining game."
}; // };
await communicationManager.BroadcastToPlayers(response, userName); // await communicationManager.BroadcastToPlayers(response, userName);
} //}
else //else
{ //{
// Other members of the game see a regular JoinGame occur. // // Other members of the game see a regular JoinGame occur.
var response = new JoinGameResponse(ClientAction.JoinGame) // var response = new JoinGameResponse(ClientAction.JoinGame)
{ // {
PlayerName = userName, // PlayerName = userName,
GameName = sessionName // GameName = sessionName
}; // };
// At this time, userName hasn't subscribed and won't receive this message. // // At this time, userName hasn't subscribed and won't receive this message.
await communicationManager.BroadcastToGame(sessionName, response); // await communicationManager.BroadcastToGame(sessionName, response);
// The player joining sees the JoinByCode occur. // // The player joining sees the JoinByCode occur.
response = new JoinGameResponse(ClientAction.JoinByCode) // response = new JoinGameResponse(ClientAction.JoinByCode)
{ // {
PlayerName = userName, // PlayerName = userName,
GameName = sessionName // GameName = sessionName
}; // };
await communicationManager.BroadcastToPlayers(response, userName); // await communicationManager.BroadcastToPlayers(response, userName);
} //}
} }
} }
} }

View File

@@ -1,13 +1,14 @@
using Gameboard.Shogi.Api.ServiceModels.Messages; 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 Newtonsoft.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{ {
public class JoinGameHandler : IActionHandler public interface IJoinGameHandler
{
Task Handle(JoinGameRequest request, string userName);
}
public class JoinGameHandler : IJoinGameHandler
{ {
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardRepository gameboardRepository;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketCommunicationManager communicationManager;
@@ -19,30 +20,30 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
} }
public async Task Handle(string json, string userName) public async Task Handle(JoinGameRequest request, string userName)
{ {
var request = JsonConvert.DeserializeObject<JoinGameRequest>(json); //var request = JsonConvert.DeserializeObject<JoinGameRequest>(json);
var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession //var joinSucceeded = await gameboardRepository.PutJoinPublicSession(new PutJoinPublicSession
{ //{
PlayerName = userName, // PlayerName = userName,
SessionName = request.GameName // SessionName = request.GameName
}); //});
var response = new JoinGameResponse(ClientAction.JoinGame) //var response = new JoinGameResponse(ClientAction.JoinGame)
{ //{
PlayerName = userName, // PlayerName = userName,
GameName = request.GameName // GameName = request.GameName
}; //};
if (joinSucceeded) //if (joinSucceeded)
{ //{
await communicationManager.BroadcastToAll(response); // await communicationManager.BroadcastToAll(response);
} //}
else //else
{ //{
response.Error = "Game is full."; // response.Error = "Game is full.";
await communicationManager.BroadcastToPlayers(response, userName); // await communicationManager.BroadcastToPlayers(response, userName);
} //}
} }
} }
} }

View File

@@ -1,16 +1,19 @@
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;
using Newtonsoft.Json;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{ {
public interface IListGamesHandler
{
Task Handle(ListGamesRequest request, string userName);
}
// TODO: This doesn't need to be a socket action. // TODO: This doesn't need to be a socket action.
// It can be an HTTP route. // It can be an HTTP route.
public class ListGamesHandler : IActionHandler public class ListGamesHandler : IListGamesHandler
{ {
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketCommunicationManager communicationManager;
private readonly IGameboardRepository repository; private readonly IGameboardRepository repository;
@@ -23,16 +26,10 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
this.repository = repository; this.repository = repository;
} }
public async Task Handle(string json, string userName) public async Task Handle(ListGamesRequest _, string userName)
{ {
var request = JsonConvert.DeserializeObject<ListGamesRequest>(json); var sessions = await repository.ReadSessions();
var getGamesResponse = string.IsNullOrWhiteSpace(userName) var games = sessions.Select(s => s.ToServiceModel()); // yuck
? await repository.GetGames()
: await repository.GetGames(userName);
var games = getGamesResponse.Sessions
.OrderBy(s => s.Player1 == userName || s.Player2 == userName)
.Select(s => new Session(s).ToServiceModel()); // yuck
var response = new ListGamesResponse(ClientAction.ListGames) var response = new ListGamesResponse(ClientAction.ListGames)
{ {

View File

@@ -3,16 +3,20 @@ 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;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{ {
public interface ILoadGameHandler
{
Task Handle(LoadGameRequest request, string userName);
}
/// <summary> /// <summary>
/// Subscribes a user to messages for a session and loads that session into the BoardManager for playing. /// Subscribes a user to messages for a session and loads that session into the BoardManager for playing.
/// </summary> /// </summary>
public class LoadGameHandler : IActionHandler public class LoadGameHandler : ILoadGameHandler
{ {
private readonly ILogger<LoadGameHandler> logger; private readonly ILogger<LoadGameHandler> logger;
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardRepository gameboardRepository;
@@ -31,26 +35,27 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
this.boardManager = boardManager; this.boardManager = boardManager;
} }
public async Task Handle(string json, string userName) public async Task Handle(LoadGameRequest request, string userName)
{ {
var request = JsonConvert.DeserializeObject<LoadGameRequest>(json); var readSession = gameboardRepository.ReadSession(request.GameName);
var gameTask = gameboardRepository.GetGame(request.GameName); var readStates = gameboardRepository.ReadBoardStates(request.GameName);
var moveTask = gameboardRepository.GetMoves(request.GameName);
var sessionModel = await gameTask; 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);
var response = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." }; var error = new LoadGameResponse(ClientAction.LoadGame) { Error = "Game not found." };
await communicationManager.BroadcastToPlayers(response, userName); await communicationManager.BroadcastToPlayers(error, userName);
return;
} }
else
{
var moveModels = await moveTask;
communicationManager.SubscribeToGame(sessionModel, userName); communicationManager.SubscribeToGame(sessionModel, userName);
var boardMoves = moveModels.Select(_ => _.ToBoardModel()).ToList(); var boardStates = await readStates;
var shogiBoard = new ShogiBoard(boardMoves); var moveModels = boardStates
.Where(_ => _.Move != null)
.Select(_ => _.Move!.ToRulesModel())
.ToList();
var shogiBoard = new ShogiBoard(moveModels);
boardManager.Add(sessionModel.Name, shogiBoard); boardManager.Add(sessionModel.Name, shogiBoard);
var response = new LoadGameResponse(ClientAction.LoadGame) var response = new LoadGameResponse(ClientAction.LoadGame)
@@ -62,4 +67,3 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
} }
} }
} }
}

View File

@@ -1,14 +1,17 @@
using Gameboard.Shogi.Api.ServiceModels.Messages; 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 Newtonsoft.Json; using Newtonsoft.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Service = Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
{ {
public class MoveHandler : IActionHandler public interface IMoveHandler
{
Task Handle(MoveRequest request, string userName);
}
public class MoveHandler : IMoveHandler
{ {
private readonly IBoardManager boardManager; private readonly IBoardManager boardManager;
private readonly IGameboardRepository gameboardRepository; private readonly IGameboardRepository gameboardRepository;
@@ -23,43 +26,43 @@ namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
} }
public async Task Handle(string json, string userName) public async Task Handle(MoveRequest request, string userName)
{ {
var request = JsonConvert.DeserializeObject<Service.Messages.MoveRequest>(json); //var request = JsonConvert.DeserializeObject<Service.Messages.MoveRequest>(json);
var moveModel = new Move(request.Move); //var moveModel = new Move(request.Move);
var board = boardManager.Get(request.GameName); //var board = boardManager.Get(request.GameName);
if (board == null) //if (board == null)
{ //{
// TODO: Find a flow for this // // TODO: Find a flow for this
var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move)
{ // {
Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first." // Error = $"Game isn't loaded. Send a message with the {Service.Types.ClientAction.LoadGame} action first."
}; // };
await communicationManager.BroadcastToPlayers(response, userName); // await communicationManager.BroadcastToPlayers(response, userName);
} //}
var boardMove = moveModel.ToBoardModel(); //var boardMove = moveModel.ToBoardModel();
var moveSuccess = board.Move(boardMove); //var moveSuccess = board.Move(boardMove);
if (moveSuccess) //if (moveSuccess)
{ //{
await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel())); // await gameboardRepository.PostMove(request.GameName, new PostMove(moveModel.ToApiModel()));
var boardState = new BoardState(board); // var boardState = new BoardState(board);
var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move)
{ // {
GameName = request.GameName, // GameName = request.GameName,
PlayerName = userName, // PlayerName = userName,
BoardState = boardState.ToServiceModel() // BoardState = boardState.ToServiceModel()
}; // };
await communicationManager.BroadcastToGame(request.GameName, response); // await communicationManager.BroadcastToGame(request.GameName, response);
} //}
else //else
{ //{
var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move) // var response = new Service.Messages.MoveResponse(Service.Types.ClientAction.Move)
{ // {
Error = "Invalid move." // Error = "Invalid move."
}; // };
await communicationManager.BroadcastToPlayers(response, userName); // await communicationManager.BroadcastToPlayers(response, userName);
} //}
} }
} }
} }

View File

@@ -3,6 +3,7 @@ using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
using Gameboard.ShogiUI.Sockets.Managers.Utility; using Gameboard.ShogiUI.Sockets.Managers.Utility;
using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -16,10 +17,9 @@ namespace Gameboard.ShogiUI.Sockets.Managers
{ {
public interface ISocketCommunicationManager public interface ISocketCommunicationManager
{ {
Task CommunicateWith(WebSocket w, string s);
Task BroadcastToAll(IResponse response); Task BroadcastToAll(IResponse response);
Task BroadcastToGame(string gameName, IResponse response); //Task BroadcastToGame(string gameName, IResponse response);
Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2);
void SubscribeToGame(Session session, string playerName); void SubscribeToGame(Session session, string playerName);
void SubscribeToBroadcast(WebSocket socket, string playerName); void SubscribeToBroadcast(WebSocket socket, string playerName);
void UnsubscribeFromBroadcastAndGames(string playerName); void UnsubscribeFromBroadcastAndGames(string playerName);
@@ -34,54 +34,14 @@ namespace Gameboard.ShogiUI.Sockets.Managers
/// <summary>Dictionary key is game name.</summary> /// <summary>Dictionary key is game name.</summary>
private readonly ConcurrentDictionary<string, Session> sessions; private readonly ConcurrentDictionary<string, Session> sessions;
private readonly ILogger<SocketCommunicationManager> logger; private readonly ILogger<SocketCommunicationManager> logger;
private readonly ActionHandlerResolver handlerResolver;
public SocketCommunicationManager( public SocketCommunicationManager(ILogger<SocketCommunicationManager> logger)
ILogger<SocketCommunicationManager> logger,
ActionHandlerResolver handlerResolver)
{ {
this.logger = logger; this.logger = logger;
this.handlerResolver = handlerResolver;
connections = new ConcurrentDictionary<string, WebSocket>(); connections = new ConcurrentDictionary<string, WebSocket>();
sessions = new ConcurrentDictionary<string, Session>(); sessions = new ConcurrentDictionary<string, Session>();
} }
public async Task CommunicateWith(WebSocket socket, string userName)
{
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.");
}
else
{
var handler = handlerResolver(request.Action);
await handler.Handle(message, userName);
}
}
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);
}
}
UnsubscribeFromBroadcastAndGames(userName);
}
public void SubscribeToBroadcast(WebSocket socket, string playerName) public void SubscribeToBroadcast(WebSocket socket, string playerName)
{ {
connections.TryAdd(playerName, socket); connections.TryAdd(playerName, socket);
@@ -154,27 +114,27 @@ namespace Gameboard.ShogiUI.Sockets.Managers
return Task.WhenAll(tasks); return Task.WhenAll(tasks);
} }
public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2)
{ //{
if (sessions.TryGetValue(gameName, out var session)) // if (sessions.TryGetValue(gameName, out var session))
{ // {
var serialized1 = JsonConvert.SerializeObject(forPlayer1); // var serialized1 = JsonConvert.SerializeObject(forPlayer1);
var serialized2 = JsonConvert.SerializeObject(forPlayer2); // var serialized2 = JsonConvert.SerializeObject(forPlayer2);
return Task.WhenAll( // return Task.WhenAll(
session.SendToPlayer1(serialized1), // session.SendToPlayer1(serialized1),
session.SendToPlayer2(serialized2)); // session.SendToPlayer2(serialized2));
} // }
return Task.CompletedTask; // return Task.CompletedTask;
} //}
public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers)
{ //{
if (sessions.TryGetValue(gameName, out var session)) // if (sessions.TryGetValue(gameName, out var session))
{ // {
var serialized = JsonConvert.SerializeObject(messageForAllPlayers); // var serialized = JsonConvert.SerializeObject(messageForAllPlayers);
return session.Broadcast(serialized); // return session.Broadcast(serialized);
} // }
return Task.CompletedTask; // return Task.CompletedTask;
} //}
} }
} }

View File

@@ -1,6 +1,14 @@
using Microsoft.AspNetCore.Http; using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
using Gameboard.ShogiUI.Sockets.Managers.Utility;
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;
using System.Net; using System.Net;
using System.Net.WebSockets;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers namespace Gameboard.ShogiUI.Sockets.Managers
@@ -12,14 +20,36 @@ namespace Gameboard.ShogiUI.Sockets.Managers
public class SocketConnectionManager : ISocketConnectionManager public class SocketConnectionManager : ISocketConnectionManager
{ {
private readonly ILogger<SocketConnectionManager> logger;
private readonly ISocketCommunicationManager communicationManager; private readonly ISocketCommunicationManager communicationManager;
private readonly ISocketTokenManager tokenManager; 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(ISocketCommunicationManager communicationManager, ISocketTokenManager tokenManager) : base() public SocketConnectionManager(
ILogger<SocketConnectionManager> logger,
ISocketCommunicationManager communicationManager,
ISocketTokenManager tokenManager,
ICreateGameHandler createGameHandler,
IJoinByCodeHandler joinByCodeHandler,
IJoinGameHandler joinGameHandler,
IListGamesHandler listGamesHandler,
ILoadGameHandler loadGameHandler,
IMoveHandler moveHandler) : base()
{ {
this.logger = logger;
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
this.tokenManager = tokenManager; this.tokenManager = tokenManager;
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 async Task HandleSocketRequest(HttpContext context)
@@ -33,7 +63,74 @@ namespace Gameboard.ShogiUI.Sockets.Managers
if (userName != null) if (userName != null)
{ {
var socket = await context.WebSockets.AcceptWebSocketAsync(); var socket = await context.WebSockets.AcceptWebSocketAsync();
await communicationManager.CommunicateWith(socket, userName);
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; return;
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -16,27 +17,24 @@ namespace Gameboard.ShogiUI.Sockets.Managers
/// <summary> /// <summary>
/// Key is userName /// Key is userName
/// </summary> /// </summary>
private readonly Dictionary<string, Guid> Tokens; private readonly ConcurrentDictionary<string, Guid> Tokens;
public SocketTokenManager() public SocketTokenManager()
{ {
Tokens = new Dictionary<string, Guid>(); Tokens = new ConcurrentDictionary<string, Guid>();
} }
public Guid GenerateToken(string userName) public Guid GenerateToken(string userName)
{ {
var guid = Guid.NewGuid(); Tokens.Remove(userName, out _);
if (Tokens.ContainsKey(userName)) var guid = Guid.NewGuid();
{ Tokens.TryAdd(userName, guid);
Tokens.Remove(userName);
}
Tokens.Add(userName, guid);
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
await Task.Delay(TimeSpan.FromMinutes(1)); await Task.Delay(TimeSpan.FromMinutes(1));
Tokens.Remove(userName); Tokens.Remove(userName, out _);
}); });
return guid; return guid;
@@ -45,13 +43,12 @@ namespace Gameboard.ShogiUI.Sockets.Managers
/// <returns>User name associated to the guid or null.</returns> /// <returns>User name associated to the guid or null.</returns>
public string GetUsername(Guid guid) public string GetUsername(Guid guid)
{ {
if (Tokens.ContainsValue(guid)) var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key;
if (userName != null)
{ {
var username = Tokens.First(kvp => kvp.Value == guid).Key; Tokens.Remove(userName, out _);
Tokens.Remove(username);
return username;
} }
return null; return userName;
} }
} }
} }

View File

@@ -6,6 +6,5 @@ namespace Gameboard.ShogiUI.Sockets.Managers.Utility
public class Request : IRequest public class Request : IRequest
{ {
public ClientAction Action { get; set; } public ClientAction Action { get; set; }
public string PlayerName { get; set; }
} }
} }

View File

@@ -1,5 +1,6 @@
using Gameboard.ShogiUI.Rules; using Gameboard.ShogiUI.Rules;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using ServiceTypes = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using ServiceTypes = Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
@@ -7,28 +8,53 @@ namespace Gameboard.ShogiUI.Sockets.Models
{ {
public class BoardState public class BoardState
{ {
public Piece[,] Board { get; set; } // TODO: Create a custom 2D array implementation which removes the (x,y) or (y,x) ambiguity.
public IReadOnlyCollection<Piece> Player1Hand { get; set; } public Piece?[,] Board { get; }
public IReadOnlyCollection<Piece> Player2Hand { get; set; } 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) public BoardState(ShogiBoard shogi)
{ {
Board = new Piece[9, 9]; Board = new 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++)
Board[x, y] = new Piece(shogi.Board[x, 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(); Player1Hand = shogi.Hands[WhichPlayer.Player1].Select(_ => new Piece(_)).ToList();
Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(_ => new Piece(_)).ToList(); Player2Hand = shogi.Hands[WhichPlayer.Player2].Select(_ => new Piece(_)).ToList();
Move = new Move(shogi.MoveHistory[^1]);
} }
public ServiceTypes.BoardState ToServiceModel() public ServiceTypes.BoardState ToServiceModel()
{ {
var board = new ServiceTypes.Piece[9, 9]; var board = new ServiceTypes.Piece[9, 9];
Board = new 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++)
board[x, y] = Board[x, y].ToServiceModel(); {
var piece = Board[x, y];
if (piece != null)
{
board[x, y] = piece.ToServiceModel();
}
}
return new ServiceTypes.BoardState return new ServiceTypes.BoardState
{ {
Board = board, Board = board,

View File

@@ -1,88 +1,41 @@
using Gameboard.ShogiUI.Rules; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Microsoft.FSharp.Core;
using System;
using System.Numerics; using System.Numerics;
using BoardStateMove = Gameboard.ShogiUI.Rules.Move;
using ShogiApi = Gameboard.Shogi.Api.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Models namespace Gameboard.ShogiUI.Sockets.Models
{ {
public class Move public class Move
{ {
public string PieceFromCaptured { get; set; } public Coords? From { get; set; }
public Coords From { get; set; }
public Coords To { get; set; }
public bool IsPromotion { get; set; } public bool IsPromotion { get; set; }
public WhichPiece? PieceFromHand { get; set; }
public Coords To { get; set; }
public Move(ServiceModels.Socket.Types.Move move) public Move(Coords from, Coords to, bool isPromotion)
{ {
From = Coords.FromBoardNotation(move.From); From = from;
To = Coords.FromBoardNotation(move.To); To = to;
PieceFromCaptured = move.PieceFromCaptured; IsPromotion = isPromotion;
IsPromotion = move.IsPromotion;
} }
public Move(ShogiApi.Move move)
public Move(WhichPiece pieceFromHand, Coords to)
{ {
string pieceFromCaptured = null; PieceFromHand = pieceFromHand;
if (move.PieceFromCaptured != null) To = to;
{
pieceFromCaptured = move.PieceFromCaptured.Value switch
{
ShogiApi.WhichPieceName.Bishop => "",
ShogiApi.WhichPieceName.GoldenGeneral => "G",
ShogiApi.WhichPieceName.King => "K",
ShogiApi.WhichPieceName.Knight => "k",
ShogiApi.WhichPieceName.Lance => "L",
ShogiApi.WhichPieceName.Pawn => "P",
ShogiApi.WhichPieceName.Rook => "R",
ShogiApi.WhichPieceName.SilverGeneral => "S",
_ => ""
};
}
From = new Coords(move.Origin.X, move.Origin.Y);
To = new Coords(move.Destination.X, move.Destination.Y);
IsPromotion = move.IsPromotion;
PieceFromCaptured = pieceFromCaptured;
} }
public ServiceModels.Socket.Types.Move ToServiceModel() => new() public ServiceModels.Socket.Types.Move ToServiceModel() => new()
{ {
From = From.ToBoardNotation(), From = From?.ToBoardNotation(),
IsPromotion = IsPromotion, IsPromotion = IsPromotion,
PieceFromCaptured = PieceFromCaptured, To = To.ToBoardNotation(),
To = To.ToBoardNotation() PieceFromCaptured = PieceFromHand
}; };
public ShogiApi.Move ToApiModel()
public Rules.Move ToRulesModel()
{ {
var pieceFromCaptured = PieceFromCaptured switch return PieceFromHand != null
{ ? new Rules.Move((Rules.WhichPiece)PieceFromHand, new Vector2(To.X, To.Y))
"B" => new FSharpOption<ShogiApi.WhichPieceName>(ShogiApi.WhichPieceName.Bishop), : new Rules.Move(new Vector2(From!.X, From.Y), new Vector2(To.X, To.Y), IsPromotion);
"G" => new FSharpOption<ShogiApi.WhichPieceName>(ShogiApi.WhichPieceName.GoldenGeneral),
"K" => new FSharpOption<ShogiApi.WhichPieceName>(ShogiApi.WhichPieceName.King),
"k" => new FSharpOption<ShogiApi.WhichPieceName>(ShogiApi.WhichPieceName.Knight),
"L" => new FSharpOption<ShogiApi.WhichPieceName>(ShogiApi.WhichPieceName.Lance),
"P" => new FSharpOption<ShogiApi.WhichPieceName>(ShogiApi.WhichPieceName.Pawn),
"R" => new FSharpOption<ShogiApi.WhichPieceName>(ShogiApi.WhichPieceName.Rook),
"S" => new FSharpOption<ShogiApi.WhichPieceName>(ShogiApi.WhichPieceName.SilverGeneral),
_ => null
};
var target = new ShogiApi.Move
{
Origin = new ShogiApi.BoardLocation { X = From.X, Y = From.Y },
Destination = new ShogiApi.BoardLocation { X = To.X, Y = To.Y },
IsPromotion = IsPromotion,
PieceFromCaptured = pieceFromCaptured
};
return target;
}
public BoardStateMove ToBoardModel()
{
return new BoardStateMove
{
From = new Vector2(From.X, From.Y),
IsPromotion = IsPromotion,
PieceFromCaptured = Enum.TryParse<WhichPiece>(PieceFromCaptured, out var whichPiece) ? whichPiece : null,
To = new Vector2(To.X, To.Y)
};
} }
} }
} }

View File

@@ -1,18 +1,25 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using BoardStatePiece = Gameboard.ShogiUI.Rules.Pieces.Piece;
namespace Gameboard.ShogiUI.Sockets.Models namespace Gameboard.ShogiUI.Sockets.Models
{ {
public class Piece public class Piece
{ {
public WhichPiece WhichPiece { get; set; } public bool IsPromoted { get; }
public WhichPlayer Owner { get; }
public WhichPiece WhichPiece { get; }
public bool IsPromoted { get; set; } public Piece(bool isPromoted, WhichPlayer owner, WhichPiece whichPiece)
{
public Piece(BoardStatePiece piece) IsPromoted = isPromoted;
Owner = owner;
WhichPiece = whichPiece;
}
public Piece(Rules.Pieces.Piece piece)
{ {
WhichPiece = (WhichPiece)piece.WhichPiece;
IsPromoted = piece.IsPromoted; IsPromoted = piece.IsPromoted;
Owner = (WhichPlayer)piece.Owner;
WhichPiece = (WhichPiece)piece.WhichPiece;
} }
public ServiceModels.Socket.Types.Piece ToServiceModel() public ServiceModels.Socket.Types.Piece ToServiceModel()
@@ -20,6 +27,7 @@ namespace Gameboard.ShogiUI.Sockets.Models
return new ServiceModels.Socket.Types.Piece return new ServiceModels.Socket.Types.Piece
{ {
IsPromoted = IsPromoted, IsPromoted = IsPromoted,
Owner = Owner,
WhichPiece = WhichPiece WhichPiece = WhichPiece
}; };
} }

View File

@@ -1,5 +1,6 @@
using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
using Newtonsoft.Json;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.WebSockets; using System.Net.WebSockets;
@@ -9,18 +10,20 @@ namespace Gameboard.ShogiUI.Sockets.Models
{ {
public class Session public class Session
{ {
[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; }
public bool IsPrivate { get; }
public ConcurrentDictionary<string, WebSocket> Subscriptions { get; } public Session(string name, bool isPrivate, string player1, string? player2 = null)
public Session(Shogi.Api.ServiceModels.Types.Session session)
{ {
Name = session.Name;
Player1 = session.Player1;
Player2 = session.Player2;
Subscriptions = new ConcurrentDictionary<string, WebSocket>(); Subscriptions = new ConcurrentDictionary<string, WebSocket>();
Name = name;
Player1 = player1;
Player2 = player2;
IsPrivate = isPrivate;
} }
public bool Subscribe(string playerName, WebSocket socket) => Subscriptions.TryAdd(playerName, socket); public bool Subscribe(string playerName, WebSocket socket) => Subscriptions.TryAdd(playerName, socket);
@@ -47,7 +50,7 @@ namespace Gameboard.ShogiUI.Sockets.Models
public Task SendToPlayer2(string message) public Task SendToPlayer2(string message)
{ {
if (Subscriptions.TryGetValue(Player2, out var socket)) if (Player2 != null && Subscriptions.TryGetValue(Player2, out var socket))
{ {
return socket.SendTextAsync(message); return socket.SendTextAsync(message);
} }

View File

@@ -0,0 +1,75 @@
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,15 @@
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class CouchCreateResult
{
public string Id { get; set; }
public bool Ok { get; set; }
public string Rev { get; set; }
public CouchCreateResult()
{
Id = string.Empty;
Rev = string.Empty;
}
}
}

View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
using System;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public abstract class CouchDocument
{
[JsonProperty("_id")]
public string Id { get; set; }
public string Type { get; set; }
public DateTimeOffset CreatedDate { get; set; }
public CouchDocument()
{
Id = string.Empty;
Type = string.Empty;
CreatedDate = DateTimeOffset.UtcNow;
}
public CouchDocument(string id, string type)
{
Id = id;
Type = type;
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
internal class CouchFindResult<T>
{
public T[] docs;
public CouchFindResult()
{
docs = Array.Empty<T>();
}
}
}

View File

@@ -0,0 +1,45 @@
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class Move
{
/// <summary>
/// A board coordinate, like A3 or G6. When null, look for PieceFromHand to exist.
/// </summary>
public string? From { get; set; }
public bool IsPromotion { get; set; }
/// <summary>
/// The piece placed from the player's hand.
/// </summary>
public WhichPiece? PieceFromHand { get; set; }
/// <summary>
/// A board coordinate, like A3 or G6.
/// </summary>
public string To { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public Move()
{
To = string.Empty;
}
public Move(Models.Move move)
{
From = move.From?.ToBoardNotation();
IsPromotion = move.IsPromotion;
To = move.To.ToBoardNotation();
PieceFromHand = move.PieceFromHand;
}
public Models.Move ToDomainModel() => PieceFromHand.HasValue
? new((ServiceModels.Socket.Types.WhichPiece)PieceFromHand, Coords.FromBoardNotation(To))
: new(Coords.FromBoardNotation(From!), Coords.FromBoardNotation(To), IsPromotion);
}
}

View File

@@ -0,0 +1,27 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class Piece
{
public bool IsPromoted { get; set; }
public WhichPlayer Owner { get; set; }
public WhichPiece WhichPiece { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public Piece()
{
}
public Piece(Models.Piece piece)
{
IsPromoted = piece.IsPromoted;
Owner = piece.Owner;
WhichPiece = piece.WhichPiece;
}
public Models.Piece ToDomainModel() => new(IsPromoted, Owner, WhichPiece);
}
}

View File

@@ -0,0 +1,4 @@
### 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

@@ -0,0 +1,30 @@
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,23 @@
using System;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class User : CouchDocument
{
public static string GetDocumentId(string userName) => $"org.couchdb.user:{userName}";
public enum LoginPlatform
{
Microsoft,
Guest
}
public string Name { get; set; }
public LoginPlatform Platform { get; set; }
public User(string name, LoginPlatform platform) : base($"org.couchdb.user:{name}", nameof(User))
{
Name = name;
Platform = platform;
}
}
}

View File

@@ -1,6 +1,5 @@
using Gameboard.Shogi.Api.ServiceModels.Messages; using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
using Gameboard.ShogiUI.Sockets.Models; using Microsoft.Extensions.Logging;
using Gameboard.ShogiUI.Sockets.Repositories.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -13,141 +12,173 @@ namespace Gameboard.ShogiUI.Sockets.Repositories
{ {
public interface IGameboardRepository public interface IGameboardRepository
{ {
Task DeleteGame(string gameName); Task<bool> CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move);
Task<Session> GetGame(string gameName); Task<bool> CreateGuestUser(string userName);
Task<GetSessionsResponse> GetGames(); Task<bool> CreateSession(Models.Session session);
Task<GetSessionsResponse> GetGames(string playerName); Task<IList<Models.Session>> ReadSessions();
Task<List<Move>> GetMoves(string gameName); Task<bool> IsGuestUser(string userName);
Task<string> PostSession(PostSession request);
Task<string> PostJoinPrivateSession(PostJoinPrivateSession request);
Task<bool> PutJoinPublicSession(PutJoinPublicSession request);
Task PostMove(string gameName, PostMove request);
Task<string> PostJoinCode(string gameName, string userName); Task<string> PostJoinCode(string gameName, string userName);
Task<Player> GetPlayer(string userName); Task<Models.Session?> ReadSession(string name);
Task<bool> PostPlayer(PostPlayer request); Task<IList<Models.BoardState>> ReadBoardStates(string name);
} }
public class GameboardRepository : IGameboardRepository public class GameboardRepository : IGameboardRepository
{ {
private const string GetSessionsRoute = "Sessions"; private const string ApplicationJson = "application/json";
private const string PostSessionRoute = "Session"; private readonly HttpClient client;
private const string JoinSessionRoute = "Session/Join"; private readonly ILogger<GameboardRepository> logger;
private const string PlayerRoute = "Player";
private const string MediaType = "application/json"; public GameboardRepository(IHttpClientFactory clientFactory, ILogger<GameboardRepository> logger)
private readonly IAuthenticatedHttpClient client;
public GameboardRepository(IAuthenticatedHttpClient client)
{ {
this.client = client; client = clientFactory.CreateClient("couchdb");
this.logger = logger;
} }
public async Task<GetSessionsResponse> GetGames() public async Task<IList<Models.Session>> ReadSessions()
{ {
var response = await client.GetAsync(GetSessionsRoute); var selector = $@"{{ ""{nameof(Session.Type)}"": ""{nameof(Session)}"" }}";
var json = await response.Content.ReadAsStringAsync(); var query = $@"{{ ""selector"": {selector} }}";
return JsonConvert.DeserializeObject<GetSessionsResponse>(json); var content = new StringContent(query, Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content);
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchFindResult<Session>>(responseContent);
if (result == null)
{
logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions));
return Array.Empty<Models.Session>();
}
return result.docs
.Select(_ => _.ToDomainModel())
.ToList();
} }
public async Task<GetSessionsResponse> GetGames(string playerName) public async Task<Models.Session?> ReadSession(string name)
{ {
var uri = $"Sessions/{playerName}"; var response = await client.GetAsync(name);
var response = await client.GetAsync(Uri.EscapeUriString(uri)); var responseContent = await response.Content.ReadAsStringAsync();
var json = await response.Content.ReadAsStringAsync(); var couchModel = JsonConvert.DeserializeObject<Session>(responseContent);
return JsonConvert.DeserializeObject<GetSessionsResponse>(json); return couchModel.ToDomainModel();
} }
public async Task<Session> GetGame(string gameName) public async Task<IList<Models.BoardState>> ReadBoardStates(string name)
{ {
var uri = $"Session/{gameName}"; var selector = $@"{{ ""{nameof(BoardState.Type)}"": ""{nameof(BoardState)}"", ""{nameof(BoardState.Name)}"": ""{name}"" }}";
var response = await client.GetAsync(Uri.EscapeUriString(uri)); var sort = $@"{{ ""{nameof(BoardState.CreatedDate)}"" : ""desc"" }}";
var json = await response.Content.ReadAsStringAsync(); var query = $@"{{ ""selector"": {selector}, ""sort"": {sort} }}";
if (string.IsNullOrWhiteSpace(json)) var content = new StringContent(query, Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync("_find", content);
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchFindResult<BoardState>>(responseContent);
if (result == null)
{ {
return null; logger.LogError("Unable to deserialize couchdb result during {0}.", nameof(this.ReadSessions));
return Array.Empty<Models.BoardState>();
} }
return new Session(JsonConvert.DeserializeObject<GetSessionResponse>(json).Session); return result.docs
.Select(_ => new Models.BoardState(_))
.ToList();
} }
public async Task DeleteGame(string gameName) //public async Task DeleteGame(string gameName)
//{
// //var uri = $"Session/{gameName}";
// //await client.DeleteAsync(Uri.EscapeUriString(uri));
//}
public async Task<bool> CreateSession(Models.Session session)
{ {
var uri = $"Session/{gameName}"; var couchModel = new Session(session.Name, session);
await client.DeleteAsync(Uri.EscapeUriString(uri)); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
} }
public async Task<string> PostSession(PostSession request) public async Task<bool> CreateBoardState(string sessionName, Models.BoardState boardState, Models.Move? move)
{ {
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); var couchModel = new BoardState(sessionName, boardState, move);
var response = await client.PostAsync(PostSessionRoute, content); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var json = await response.Content.ReadAsStringAsync(); var response = await client.PostAsync(string.Empty, content);
return JsonConvert.DeserializeObject<PostSessionResponse>(json).SessionName; 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);
var response = await client.PutAsync(JoinSessionRoute, content); // var response = await client.PutAsync(JoinSessionRoute, content);
var json = await response.Content.ReadAsStringAsync(); // var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<PutJoinPublicSessionResponse>(json).JoinSucceeded; // return JsonConvert.DeserializeObject<PutJoinPublicSessionResponse>(json).JoinSucceeded;
} //}
public async Task<string> PostJoinPrivateSession(PostJoinPrivateSession request) //public async Task<string> PostJoinPrivateSession(PostJoinPrivateSession request)
{ //{
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
var response = await client.PostAsync(JoinSessionRoute, content); // var response = await client.PostAsync(JoinSessionRoute, content);
var json = await response.Content.ReadAsStringAsync(); // var json = await response.Content.ReadAsStringAsync();
var deserialized = JsonConvert.DeserializeObject<PostJoinPrivateSessionResponse>(json); // var deserialized = JsonConvert.DeserializeObject<PostJoinPrivateSessionResponse>(json);
if (deserialized.JoinSucceeded) // if (deserialized.JoinSucceeded)
{ // {
return deserialized.SessionName; // return deserialized.SessionName;
} // }
return null; // return null;
} //}
public async Task<List<Move>> GetMoves(string gameName) //public async Task<List<Move>> GetMoves(string gameName)
{ //{
var uri = $"Session/{gameName}/Moves"; // var uri = $"Session/{gameName}/Moves";
var get = await client.GetAsync(Uri.EscapeUriString(uri)); // var get = await client.GetAsync(Uri.EscapeUriString(uri));
var json = await get.Content.ReadAsStringAsync(); // var json = await get.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(json)) // if (string.IsNullOrWhiteSpace(json))
{ // {
return new List<Move>(); // return new List<Move>();
} // }
var response = JsonConvert.DeserializeObject<GetMovesResponse>(json); // var response = JsonConvert.DeserializeObject<GetMovesResponse>(json);
return response.Moves.Select(m => new Move(m)).ToList(); // return response.Moves.Select(m => new Move(m)).ToList();
} //}
public async Task PostMove(string gameName, PostMove request) //public async Task PostMove(string gameName, PostMove request)
{ //{
var uri = $"Session/{gameName}/Move"; // var uri = $"Session/{gameName}/Move";
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
await client.PostAsync(Uri.EscapeUriString(uri), content); // await client.PostAsync(Uri.EscapeUriString(uri), content);
} //}
public async Task<string> PostJoinCode(string gameName, string userName) public async Task<string> PostJoinCode(string gameName, string userName)
{ {
var uri = $"JoinCode/{gameName}"; // var uri = $"JoinCode/{gameName}";
var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); // var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName });
var content = new StringContent(serialized, Encoding.UTF8, MediaType); // var content = new StringContent(serialized, Encoding.UTF8, MediaType);
var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); // var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<PostJoinCodeResponse>(json).JoinCode; // return JsonConvert.DeserializeObject<PostJoinCodeResponse>(json).JoinCode;
return string.Empty;
} }
public async Task<Player> GetPlayer(string playerName) //public async Task<Player> GetPlayer(string playerName)
//{
// var uri = $"Player/{playerName}";
// var get = await client.GetAsync(Uri.EscapeUriString(uri));
// var content = await get.Content.ReadAsStringAsync();
// if (!string.IsNullOrWhiteSpace(content))
// {
// var response = JsonConvert.DeserializeObject<GetPlayerResponse>(content);
// return new Player(response.Player.Name);
// }
// return null;
//}
public async Task<bool> CreateGuestUser(string userName)
{ {
var uri = $"Player/{playerName}"; var couchModel = new User(userName, User.LoginPlatform.Guest);
var get = await client.GetAsync(Uri.EscapeUriString(uri)); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var content = await get.Content.ReadAsStringAsync(); var response = await client.PostAsync(string.Empty, content);
if (!string.IsNullOrWhiteSpace(content)) return response.IsSuccessStatusCode;
{
var response = JsonConvert.DeserializeObject<GetPlayerResponse>(content);
return new Player(response.Player.Name);
}
return null;
} }
public async Task<bool> PostPlayer(PostPlayer request) public async Task<bool> IsGuestUser(string userName)
{ {
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); var req = new HttpRequestMessage(HttpMethod.Head, new Uri($"{client.BaseAddress}/{User.GetDocumentId(userName)}"));
var response = await client.PostAsync(PlayerRoute, content); var response = await client.SendAsync(req);
return response.IsSuccessStatusCode; return response.IsSuccessStatusCode;
} }
} }

View File

@@ -1,4 +1,4 @@
using Gameboard.Shogi.Api.ServiceModels.Messages; using Gameboard.ShogiUI.Sockets.Models;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -9,7 +9,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers
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> PlayerExists(string playerName); Task<bool> CreateSession(Session session);
} }
public class GameboardRepositoryManager : IGameboardRepositoryManager public class GameboardRepositoryManager : IGameboardRepositoryManager
@@ -30,11 +30,7 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers
{ {
count++; count++;
var clientId = $"Guest-{Guid.NewGuid()}"; var clientId = $"Guest-{Guid.NewGuid()}";
var request = new PostPlayer var isCreated = await repository.CreateGuestUser(clientId);
{
PlayerName = clientId
};
var isCreated = await repository.PostPlayer(request);
if (isCreated) if (isCreated)
{ {
return clientId; return clientId;
@@ -45,22 +41,31 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers
public async Task<bool> IsPlayer1(string sessionName, string playerName) public async Task<bool> IsPlayer1(string sessionName, string playerName)
{ {
var session = await repository.GetGame(sessionName); //var session = await repository.GetGame(sessionName);
return session?.Player1 == playerName; //return session?.Player1 == playerName;
return true;
} }
public async Task<string> CreateJoinCode(string sessionName, string playerName) public async Task<string> CreateJoinCode(string sessionName, string playerName)
{ {
var session = await repository.GetGame(sessionName); //var session = await repository.GetGame(sessionName);
if (playerName == session?.Player1) //if (playerName == session?.Player1)
{ //{
return await repository.PostJoinCode(sessionName, playerName); // return await repository.PostJoinCode(sessionName, playerName);
} //}
return null; return null;
} }
public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix); public async Task<bool> CreateSession(Session session)
{
var success = await repository.CreateSession(session);
if (success)
{
return await repository.CreateBoardState(session.Name, new BoardState(), null);
}
return false;
}
public async Task<bool> PlayerExists(string playerName) => await repository.GetPlayer(playerName) != null; public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix);
} }
} }

View File

@@ -1,122 +0,0 @@
using IdentityModel.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Repositories.Utility
{
public interface IAuthenticatedHttpClient
{
Task<HttpResponseMessage> DeleteAsync(string requestUri);
Task<HttpResponseMessage> GetAsync(string requestUri);
Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content);
}
public class AuthenticatedHttpClient : HttpClient, IAuthenticatedHttpClient
{
private readonly ILogger<AuthenticatedHttpClient> logger;
private readonly string identityServerUrl;
private TokenResponse tokenResponse;
private readonly string clientId;
private readonly string clientSecret;
public AuthenticatedHttpClient(ILogger<AuthenticatedHttpClient> logger, IConfiguration configuration) : base()
{
this.logger = logger;
identityServerUrl = configuration["AppSettings:IdentityServer"];
clientId = configuration["AppSettings:ClientId"];
clientSecret = configuration["AppSettings:ClientSecret"];
BaseAddress = new Uri(configuration["AppSettings:GameboardShogiApi"]);
}
private async Task RefreshBearerToken()
{
var disco = await this.GetDiscoveryDocumentAsync(identityServerUrl);
if (disco.IsError)
{
logger.LogError("{DiscoveryErrorType}", disco.ErrorType);
throw new Exception(disco.Error);
}
var request = new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = clientId,
ClientSecret = clientSecret
};
var response = await this.RequestClientCredentialsTokenAsync(request);
if (response.IsError)
{
throw new Exception(response.Error);
}
tokenResponse = response;
logger.LogInformation("Refreshing Bearer Token to {BaseAddress}", BaseAddress);
this.SetBearerToken(tokenResponse.AccessToken);
}
public new async Task<HttpResponseMessage> GetAsync(string requestUri)
{
var response = await base.GetAsync(requestUri);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await RefreshBearerToken();
response = await base.GetAsync(requestUri);
}
logger.LogInformation(
"Repository GET to {BaseUrl}{RequestUrl} \nResponse: {Response}\n",
BaseAddress,
requestUri,
await response.Content.ReadAsStringAsync());
return response;
}
public new async Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
{
var response = await base.PostAsync(requestUri, content);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await RefreshBearerToken();
response = await base.PostAsync(requestUri, content);
}
logger.LogInformation(
"Repository POST to {BaseUrl}{RequestUrl} \n\tRespCode: {RespCode} \n\tRequest: {Request}\n\tResponse: {Response}\n",
BaseAddress,
requestUri,
response.StatusCode,
await content.ReadAsStringAsync(),
await response.Content.ReadAsStringAsync());
return response;
}
public new async Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content)
{
var response = await base.PutAsync(requestUri, content);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await RefreshBearerToken();
response = await base.PutAsync(requestUri, content);
}
logger.LogInformation(
"Repository PUT to {BaseUrl}{RequestUrl} \n\tRespCode: {RespCode} \n\tRequest: {Request}\n\tResponse: {Response}\n",
BaseAddress,
requestUri,
response.StatusCode,
await content.ReadAsStringAsync(),
await response.Content.ReadAsStringAsync());
return response;
}
public new async Task<HttpResponseMessage> DeleteAsync(string requestUri)
{
var response = await base.DeleteAsync(requestUri);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await RefreshBearerToken();
response = await base.DeleteAsync(requestUri);
}
logger.LogInformation("Repository DELETE to {BaseUrl}{RequestUrl}", BaseAddress, requestUri);
return response;
}
}
}

View File

@@ -3,8 +3,6 @@ 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.Repositories.RepositoryManagers;
using Gameboard.ShogiUI.Sockets.Repositories.Utility;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
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;
@@ -15,8 +13,8 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
namespace Gameboard.ShogiUI.Sockets namespace Gameboard.ShogiUI.Sockets
{ {
@@ -33,36 +31,33 @@ namespace Gameboard.ShogiUI.Sockets
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
// Socket ActionHandlers // Socket ActionHandlers
services.AddSingleton<CreateGameHandler>(); services.AddSingleton<ICreateGameHandler, CreateGameHandler>();
services.AddSingleton<JoinByCodeHandler>(); services.AddSingleton<IJoinByCodeHandler, JoinByCodeHandler>();
services.AddSingleton<JoinGameHandler>(); services.AddSingleton<IJoinGameHandler, JoinGameHandler>();
services.AddSingleton<ListGamesHandler>(); services.AddSingleton<IListGamesHandler, ListGamesHandler>();
services.AddSingleton<LoadGameHandler>(); services.AddSingleton<ILoadGameHandler, LoadGameHandler>();
services.AddSingleton<MoveHandler>(); services.AddSingleton<IMoveHandler, MoveHandler>();
// Managers // Managers
services.AddSingleton<ISocketCommunicationManager, SocketCommunicationManager>(); services.AddSingleton<ISocketCommunicationManager, SocketCommunicationManager>();
services.AddSingleton<ISocketTokenManager, SocketTokenManager>(); services.AddSingleton<ISocketTokenManager, SocketTokenManager>();
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>(); services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
services.AddScoped<IGameboardRepositoryManager, GameboardRepositoryManager>(); services.AddSingleton<IGameboardRepositoryManager, GameboardRepositoryManager>();
services.AddSingleton<IBoardManager, BoardManager>(); services.AddSingleton<IBoardManager, BoardManager>();
services.AddSingleton<ActionHandlerResolver>(sp => action =>
{
return action switch
{
ClientAction.ListGames => sp.GetService<ListGamesHandler>(),
ClientAction.CreateGame => sp.GetService<CreateGameHandler>(),
ClientAction.JoinGame => sp.GetService<JoinGameHandler>(),
ClientAction.JoinByCode => sp.GetService<JoinByCodeHandler>(),
ClientAction.LoadGame => sp.GetService<LoadGameHandler>(),
ClientAction.Move => sp.GetService<MoveHandler>(),
_ => throw new KeyNotFoundException($"Unable to resolve {nameof(IActionHandler)} for {nameof(ClientAction)} {action}"),
};
});
// Repositories // Repositories
services.AddHttpClient("couchdb", c =>
{
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin"));
c.DefaultRequestHeaders.Add("Accept", "application/json");
c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}");
var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/";
c.BaseAddress = new Uri(baseUrl);
});
services.AddTransient<IGameboardRepository, GameboardRepository>(); services.AddTransient<IGameboardRepository, GameboardRepository>();
services.AddSingleton<IAuthenticatedHttpClient, AuthenticatedHttpClient>(); //services.AddSingleton<IAuthenticatedHttpClient, AuthenticatedHttpClient>();
//services.AddSingleton<ICouchClient>(provider => new CouchClient(databaseName, couchUrl));
services.AddControllers(); services.AddControllers();

View File

@@ -1,10 +1,9 @@
{ {
"AppSettings": { "AppSettings": {
"IdentityServer": "https://identity.lucaserver.space/", "CouchDB": {
"GameboardShogiApi": "https://dev.lucaserver.space/Gameboard.Shogi.Api/", "Database": "shogi-dev",
"ClientId": "DevClientId", "Url": "http://192.168.1.15:5984"
"ClientSecret": "DevSecret", }
"Scope": "DevEnvironment"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {

View File

@@ -13,7 +13,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
var name = self.WhichPiece switch var name = self.WhichPiece switch
{ {
WhichPiece.King => " K ", WhichPiece.King => " K ",
WhichPiece.GoldenGeneral => " G ", WhichPiece.GoldGeneral => " G ",
WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ", WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ",
WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ", WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ",
WhichPiece.Rook => self.IsPromoted ? "^R " : " R ", WhichPiece.Rook => self.IsPromoted ? "^R " : " R ",

View File

@@ -26,9 +26,9 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
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[1, 0].WhichPiece.Should().Be(WhichPiece.Knight);
board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[2, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); board[3, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[4, 0].WhichPiece.Should().Be(WhichPiece.King); board[4, 0].WhichPiece.Should().Be(WhichPiece.King);
board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); board[5, 0].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[6, 0].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight); board[7, 0].WhichPiece.Should().Be(WhichPiece.Knight);
board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance); board[8, 0].WhichPiece.Should().Be(WhichPiece.Lance);
@@ -51,9 +51,9 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance); board[0, 8].WhichPiece.Should().Be(WhichPiece.Lance);
board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[1, 8].WhichPiece.Should().Be(WhichPiece.Knight);
board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[2, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); board[3, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[4, 8].WhichPiece.Should().Be(WhichPiece.King); board[4, 8].WhichPiece.Should().Be(WhichPiece.King);
board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldenGeneral); board[5, 8].WhichPiece.Should().Be(WhichPiece.GoldGeneral);
board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral); board[6, 8].WhichPiece.Should().Be(WhichPiece.SilverGeneral);
board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight); board[7, 8].WhichPiece.Should().Be(WhichPiece.Knight);
board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance); board[8, 8].WhichPiece.Should().Be(WhichPiece.Lance);
@@ -256,23 +256,23 @@ 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[7, 0].Should().BeNull();
var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 0) }); var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = 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[7, 0].Should().BeNull();
dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 1) }); dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Knight, To = 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[7, 1].Should().BeNull();
/// try illegally placing Pawn from the hand /// try illegally placing Pawn from the hand
dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Pawn, To = new Vector2(7, 0) }); dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Pawn, To = 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[7, 0].Should().BeNull();
/// try illegally placing Lance from the hand /// try illegally placing Lance from the hand
dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(7, 0) }); dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = 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[7, 0].Should().BeNull();
@@ -312,7 +312,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
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 { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(4, 4) }); var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Lance, To = new Vector2(4, 4) });
// Assert // Assert
dropSuccess.Should().BeFalse(); dropSuccess.Should().BeFalse();
@@ -347,7 +347,7 @@ namespace Gameboard.ShogiUI.UnitTests.Rules
shogi.Board[4, 0].Should().NotBeNull(); shogi.Board[4, 0].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 { PieceFromCaptured = WhichPiece.Bishop, To = new Vector2(4, 0) }); var dropSuccess = shogi.Move(new Move { PieceFromHand = WhichPiece.Bishop, To = new Vector2(4, 0) });
// Assert // Assert
dropSuccess.Should().BeFalse(); dropSuccess.Should().BeFalse();

View File

@@ -4,7 +4,7 @@ namespace PathFinding
{ {
public interface IPlanarCollection<T> : IEnumerable<T> where T : IPlanarElement public interface IPlanarCollection<T> : IEnumerable<T> where T : IPlanarElement
{ {
T this[float x, float y] { get; set; } T? this[float x, float y] { get; set; }
int GetLength(int dimension); int GetLength(int dimension);
} }
} }

View File

@@ -4,6 +4,7 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel> <AnalysisLevel>5</AnalysisLevel>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
</Project> </Project>