diff --git a/.gitignore b/.gitignore
index 26787d3..70b8a69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,4 @@ Thumbs.db
#Luke
bin
obj
+*.user
diff --git a/Benchmarking/Benchmarking.csproj b/Benchmarking/Benchmarking.csproj
new file mode 100644
index 0000000..a2ca11e
--- /dev/null
+++ b/Benchmarking/Benchmarking.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ true
+ Exe
+
+
+
+
+
+
+
diff --git a/Benchmarking/Benchmarks.cs b/Benchmarking/Benchmarks.cs
new file mode 100644
index 0000000..4270a3e
--- /dev/null
+++ b/Benchmarking/Benchmarks.cs
@@ -0,0 +1,106 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Running;
+using System;
+using System.Linq;
+using System.Numerics;
+
+namespace Benchmarking
+{
+ public class Benchmarks
+ {
+ private readonly Vector2[] directions;
+ // Consumer is for IEnumerables.
+ private readonly Consumer consumer = new();
+
+ public Benchmarks()
+ {
+ //moves = new[]
+ //{
+ // // P1 Rook
+ // new Move { From = new Vector2(7, 1), To = new Vector2(4, 1) },
+ // // P2 Gold
+ // new Move { From = new Vector2(3, 8), To = new Vector2(2, 7) },
+ // // P1 Pawn
+ // new Move { From = new Vector2(4, 2), To = new Vector2(4, 3) },
+ // // P2 other Gold
+ // new Move { From = new Vector2(5, 8), To = new Vector2(6, 7) },
+ // // P1 same Pawn
+ // new Move { From = new Vector2(4, 3), To = new Vector2(4, 4) },
+ // // P2 Pawn
+ // new Move { From = new Vector2(4, 6), To = new Vector2(4, 5) },
+ // // P1 Pawn takes P2 Pawn
+ // new Move { From = new Vector2(4, 4), To = new Vector2(4, 5) },
+ // // P2 King
+ // new Move { From = new Vector2(4, 8), To = new Vector2(4, 7) },
+ // // P1 Pawn promotes
+ // new Move { From = new Vector2(4, 5), To = new Vector2(4, 6), IsPromotion = true },
+ // // P2 King retreat
+ // new Move { From = new Vector2(4, 7), To = new Vector2(4, 8) },
+ //};
+ //var rand = new Random();
+
+ //directions = new Vector2[10];
+ //for (var n = 0; n < 10; n++) directions[n] = new Vector2(rand.Next(-2, 2), rand.Next(-2, 2));
+ }
+
+ [Benchmark]
+ public void One()
+ {
+ for(var i=0; i<10000; i++)
+ {
+ Guid.NewGuid();
+ }
+ }
+
+ //[Benchmark]
+ public void Two()
+ {
+ }
+
+
+
+ public Vector2 FindDirection(Vector2[] directions, Vector2 destination)
+ {
+ var smallerDistance = float.MaxValue;
+ Vector2 found = Vector2.Zero;
+ foreach (var d in directions)
+ {
+ var distance = Vector2.Distance(d, destination);
+ if (distance < smallerDistance)
+ {
+ smallerDistance = distance;
+ found = d;
+ }
+ }
+ return found;
+ }
+
+
+ public Vector2 FindDirectionLinq(Vector2[] directions, Vector2 destination) =>
+ directions.Aggregate((a, b) => Vector2.Distance(destination, a) < Vector2.Distance(destination, b) ? a : b);
+
+
+
+
+ [Benchmark]
+ public void Directions_A()
+ {
+ FindDirection(directions, new Vector2(8, 7));
+ }
+ [Benchmark]
+ public void Directions_B()
+ {
+ FindDirectionLinq(directions, new Vector2(8, 7));
+ }
+ }
+
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ BenchmarkRunner.Run();
+ Console.WriteLine("Done");
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs
new file mode 100644
index 0000000..90fc455
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
+{
+ public class GetGuestTokenResponse
+ {
+ public string UserId { get; }
+ public string DisplayName { get; }
+ public Guid OneTimeToken { get; }
+
+ public GetGuestTokenResponse(string id, string displayName, Guid token)
+ {
+ UserId = id;
+ DisplayName = displayName;
+ OneTimeToken = token;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs
new file mode 100644
index 0000000..8fdbfe4
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs
@@ -0,0 +1,16 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using System.Collections.Generic;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
+{
+ public class GetSessionResponse
+ {
+ public Game Game { get; set; }
+ ///
+ /// The perspective on the game of the requesting user.
+ ///
+ public WhichPerspective PlayerPerspective { get; set; }
+ public BoardState BoardState { get; set; }
+ public IList MoveHistory { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs
new file mode 100644
index 0000000..cc972bc
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs
@@ -0,0 +1,11 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using System.Collections.ObjectModel;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
+{
+ public class GetSessionsResponse
+ {
+ public Collection PlayerHasJoinedSessions { get; set; }
+ public Collection AllOtherSessions { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs
new file mode 100644
index 0000000..acc03a9
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
+{
+ public class GetTokenResponse
+ {
+ public Guid OneTimeToken { get; }
+
+ public GetTokenResponse(Guid token)
+ {
+ OneTimeToken = token;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs
deleted file mode 100644
index 6f6a751..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
-{
- public class GetGuestToken
- {
- public string ClientId { get; set; }
- }
-
- public class GetGuestTokenResponse
- {
- public string ClientId { get; }
- public Guid OneTimeToken { get; }
-
- public GetGuestTokenResponse(string clientId, Guid token)
- {
- ClientId = clientId;
- OneTimeToken = token;
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs
deleted file mode 100644
index e2f3c98..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
-{
- public class GetTokenResponse
- {
- public Guid OneTimeToken { get; }
-
- public GetTokenResponse(Guid token)
- {
- OneTimeToken = token;
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs
similarity index 85%
rename from Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs
rename to Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs
index 10850f6..acb270f 100644
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs
@@ -1,4 +1,4 @@
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
{
public class PostGameInvitation
{
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
new file mode 100644
index 0000000..012c6ea
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
@@ -0,0 +1,11 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using System.ComponentModel.DataAnnotations;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
+{
+ public class PostMove
+ {
+ [Required]
+ public Move Move { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs
new file mode 100644
index 0000000..ebe9665
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs
@@ -0,0 +1,8 @@
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
+{
+ public class PostSession
+ {
+ public string Name { get; set; }
+ public bool IsPrivate { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj
index f208d30..ec1c55c 100644
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj
@@ -1,7 +1,10 @@
- net5.0
+ net6.0
+ true
+ 5
+ enable
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs
new file mode 100644
index 0000000..e1b59bb
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs
@@ -0,0 +1,21 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using System;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
+{
+ public class CreateGameResponse : IResponse
+ {
+ public string Action { get; }
+ public Game Game { get; set; }
+
+ ///
+ /// The player who created the game.
+ ///
+ public string PlayerName { get; set; }
+
+ public CreateGameResponse()
+ {
+ Action = ClientAction.CreateGame.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs
new file mode 100644
index 0000000..aca4cc7
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs
@@ -0,0 +1,9 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
+{
+ public interface IRequest
+ {
+ ClientAction Action { get; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs
new file mode 100644
index 0000000..69d18c6
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs
@@ -0,0 +1,7 @@
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
+{
+ public interface IResponse
+ {
+ string Action { get; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs
deleted file mode 100644
index ce8c0a4..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces
-{
- public interface IRequest
- {
- ClientAction Action { get; set; }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs
deleted file mode 100644
index 8c1bed8..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces
-{
- public interface IResponse
- {
- string Action { get; }
- string Error { get; set; }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs
new file mode 100644
index 0000000..22fac9c
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs
@@ -0,0 +1,41 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
+{
+ public class JoinByCodeRequest : IRequest
+ {
+ public ClientAction Action { get; set; }
+ public string JoinCode { get; set; } = "";
+ }
+
+ public class JoinGameRequest : IRequest
+ {
+ public ClientAction Action { get; set; }
+ public string GameName { get; set; } = "";
+ }
+
+ public class JoinGameResponse : IResponse
+ {
+ public string Action { get; protected set; }
+ public string GameName { get; set; }
+ ///
+ /// The player who joined the game.
+ ///
+ public string PlayerName { get; set; }
+
+ public JoinGameResponse()
+ {
+ Action = ClientAction.JoinGame.ToString();
+ GameName = "";
+ PlayerName = "";
+ }
+ }
+
+ public class JoinByCodeResponse : JoinGameResponse, IResponse
+ {
+ public JoinByCodeResponse()
+ {
+ Action = ClientAction.JoinByCode.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs
deleted file mode 100644
index 09d7455..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs
+++ /dev/null
@@ -1,25 +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 CreateGameRequest : IRequest
- {
- public ClientAction Action { get; set; }
- public string GameName { get; set; }
- public bool IsPrivate { get; set; }
- }
-
- public class CreateGameResponse : IResponse
- {
- public string Action { get; private set; }
- public string Error { get; set; }
- public Game Game { get; set; }
- public string PlayerName { get; set; }
-
- public CreateGameResponse(ClientAction action)
- {
- Action = action.ToString();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs
deleted file mode 100644
index 376ffc8..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs
deleted file mode 100644
index 51f2f8d..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs
deleted file mode 100644
index ef8842d..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs
+++ /dev/null
@@ -1,24 +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 JoinGameRequest : IRequest
- {
- public ClientAction Action { get; set; }
- public string GameName { get; set; }
- }
-
- public class JoinGameResponse : IResponse
- {
- public string Action { get; private set; }
- public string Error { get; set; }
- public string GameName { get; set; }
- public string PlayerName { get; set; }
-
- public JoinGameResponse(ClientAction action)
- {
- Action = action.ToString();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs
deleted file mode 100644
index 1f6541f..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using System.Collections.Generic;
-
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
-{
- public class ListGamesRequest : IRequest
- {
- public ClientAction Action { get; set; }
- }
-
- public class ListGamesResponse : IResponse
- {
- public string Action { get; private set; }
- public string Error { get; set; }
- public IEnumerable Games { get; set; }
-
- public ListGamesResponse(ClientAction action)
- {
- Action = action.ToString();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs
deleted file mode 100644
index 19e6c08..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using System.Collections.Generic;
-
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages
-{
- public class LoadGameRequest : IRequest
- {
- public ClientAction Action { get; set; }
- public string GameName { get; set; }
- }
-
- public class LoadGameResponse : IResponse
- {
- public string Action { get; private set; }
- public Game Game { get; set; }
- public IEnumerable Moves { get; set; }
- public string Error { get; set; }
-
- public LoadGameResponse(ClientAction action)
- {
- Action = action.ToString();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs
deleted file mode 100644
index 5039196..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs
+++ /dev/null
@@ -1,26 +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 MoveRequest : IRequest
- {
- public ClientAction Action { get; set; }
- public string GameName { get; set; }
- public Move Move { get; set; }
- }
-
- public class MoveResponse : IResponse
- {
- public string Action { get; }
- public string Error { get; set; }
- public string GameName { get; set; }
- public Move Move { get; set; }
- public string PlayerName { get; set; }
-
- public MoveResponse(ClientAction action)
- {
- Action = action.ToString();
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs
new file mode 100644
index 0000000..0b19a44
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs
@@ -0,0 +1,19 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
+{
+ public class MoveResponse : IResponse
+ {
+ public string Action { get; }
+ public string GameName { get; set; }
+ ///
+ /// The player that made the move.
+ ///
+ public string PlayerName { get; set; }
+
+ public MoveResponse()
+ {
+ Action = ClientAction.Move.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs
deleted file mode 100644
index 9e0952e..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
-{
- public enum ClientAction
- {
- ListGames,
- CreateGame,
- JoinGame,
- JoinByCode,
- LoadGame,
- Move,
- KeepAlive
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs
deleted file mode 100644
index d64dae4..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
-{
- public class Coords
- {
- public int X { get; set; }
- public int Y { get; set; }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs
deleted file mode 100644
index a4a2ebe..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
-{
- public class Game
- {
- public string GameName { get; set; }
- public string[] Players { get; set; }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs
deleted file mode 100644
index f815b00..0000000
--- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
-{
- public class Move
- {
- public string PieceFromCaptured { get; set; }
- public Coords From { get; set; }
- public Coords To { get; set; }
- public bool IsPromotion { get; set; }
-
- ///
- /// Toggles perspective of this move. (ie from player 1 to player 2)
- ///
- public static Move ConvertPerspective(Move m)
- {
- var convertedMove = new Move
- {
- To = new Coords
- {
- X = 8 - m.To.X,
- Y = 8 - m.To.Y
- },
- From = new Coords
- {
- X = 8 - m.From.X,
- Y = 8 - m.From.Y
- },
- IsPromotion = m.IsPromotion,
- PieceFromCaptured = m.PieceFromCaptured
- };
- return convertedMove;
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs
new file mode 100644
index 0000000..b6d0a98
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
+{
+ public class BoardState
+ {
+ public Dictionary Board { get; set; } = new Dictionary();
+ public IReadOnlyCollection Player1Hand { get; set; } = Array.Empty();
+ public IReadOnlyCollection Player2Hand { get; set; } = Array.Empty();
+ public WhichPerspective? PlayerInCheck { get; set; }
+ public WhichPerspective WhoseTurn { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs
new file mode 100644
index 0000000..99b9446
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs
@@ -0,0 +1,10 @@
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
+{
+ public enum ClientAction
+ {
+ CreateGame,
+ JoinGame,
+ JoinByCode,
+ Move
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs
new file mode 100644
index 0000000..5b70ad8
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
+{
+ public class Game
+ {
+ public string Player1 { get; set; }
+ public string? Player2 { get; set; }
+ public string GameName { get; set; } = string.Empty;
+
+ ///
+ /// Players[0] is the session owner, Players[1] is the other person.
+ ///
+ public IReadOnlyList Players
+ {
+ get
+ {
+ var list = new List(2) { Player1 };
+ if (!string.IsNullOrEmpty(Player2)) list.Add(Player2);
+ return list;
+ }
+ }
+
+ ///
+ /// Constructor for serialization.
+ ///
+ public Game()
+ {
+ }
+
+ public Game(string gameName, string player1, string? player2 = null)
+ {
+ GameName = gameName;
+ Player1 = player1;
+ Player2 = player2;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs
new file mode 100644
index 0000000..7116648
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs
@@ -0,0 +1,12 @@
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
+{
+ public class Move
+ {
+ public WhichPiece? PieceFromCaptured { get; set; }
+ /// Board position notation, like A3 or G1
+ public string? From { get; set; }
+ /// Board position notation, like A3 or G1
+ public string To { get; set; } = string.Empty;
+ public bool IsPromotion { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs
new file mode 100644
index 0000000..8e28d04
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs
@@ -0,0 +1,9 @@
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
+{
+ public class Piece
+ {
+ public bool IsPromoted { get; set; }
+ public WhichPiece WhichPiece { get; set; }
+ public WhichPerspective Owner { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs
new file mode 100644
index 0000000..8e9a72a
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs
@@ -0,0 +1,9 @@
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
+{
+ public class User
+ {
+ public string Id { get; set; } = string.Empty;
+
+ public string Name { get; set; } = string.Empty;
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs
new file mode 100644
index 0000000..cf8a4c4
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs
@@ -0,0 +1,9 @@
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
+{
+ public enum WhichPerspective
+ {
+ Player1,
+ Player2,
+ Spectator
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs
new file mode 100644
index 0000000..214bdba
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs
@@ -0,0 +1,14 @@
+namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
+{
+ public enum WhichPiece
+ {
+ King,
+ GoldGeneral,
+ SilverGeneral,
+ Bishop,
+ Rook,
+ Knight,
+ Lance,
+ Pawn
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln
index 85cdd75..3b54d4e 100644
--- a/Gameboard.ShogiUI.Sockets.sln
+++ b/Gameboard.ShogiUI.Sockets.sln
@@ -1,12 +1,26 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30503.244
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets", "Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj", "{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.ServiceModels", "Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj", "{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.UnitTests", "Gameboard.ShogiUI.UnitTests\Gameboard.ShogiUI.UnitTests.csproj", "{DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Benchmarking\Benchmarking.csproj", "{DADFF5D6-581F-4D69-845D-53ABD6ABF62F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PathFinding", "PathFinding\PathFinding.csproj", "{A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.xUnitTests", "Gameboard.ShogiUI.xUnitTests\Gameboard.ShogiUI.xUnitTests.csproj", "{12530716-C11E-40CE-9F71-CCCC243F03E1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,10 +35,39 @@ Global
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU
+ {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}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DADFF5D6-581F-4D69-845D-53ABD6ABF62F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A0AC8C5A-6ADA-45C6-BD1E-EB1061213E47}.Release|Any CPU.Build.0 = Release|Any CPU
+ {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {12530716-C11E-40CE-9F71-CCCC243F03E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {12530716-C11E-40CE-9F71-CCCC243F03E1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0211B1E4-20F0-4058-AAC4-3845D19910AF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F256989E-B6AF-4731-9DB4-88991C40B2CE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {DC8A933A-DBCB-46B9-AA0B-7B3DC9E763F3} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
+ {12530716-C11E-40CE-9F71-CCCC243F03E1} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
+ {F256989E-B6AF-4731-9DB4-88991C40B2CE} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}
EndGlobalSection
diff --git a/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs b/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs
new file mode 100644
index 0000000..f39e649
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs
@@ -0,0 +1,39 @@
+namespace Gameboard.ShogiUI.Sockets
+{
+ namespace anonymous_session.Middlewares
+ {
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Authentication;
+ using System.Security.Claims;
+
+ ///
+ /// TODO: Use this example in the guest session logic instead of custom claims.
+ ///
+ public class AnonymousSessionMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ public AnonymousSessionMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ public async System.Threading.Tasks.Task InvokeAsync(HttpContext context)
+ {
+ if (!context.User.Identity.IsAuthenticated)
+ {
+ if (string.IsNullOrEmpty(context.User.FindFirstValue(ClaimTypes.Anonymous)))
+ {
+ var claim = new Claim(ClaimTypes.Anonymous, System.Guid.NewGuid().ToString());
+ context.User.AddIdentity(new ClaimsIdentity(new[] { claim }));
+
+ string scheme = Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme;
+ await context.SignInAsync(scheme, context.User, new AuthenticationProperties { IsPersistent = false });
+ }
+ }
+
+ await _next(context);
+ }
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs
index 5d0becd..0f76e3b 100644
--- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs
+++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs
@@ -1,61 +1,269 @@
-using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages;
+using Gameboard.ShogiUI.Sockets.Extensions;
+using Gameboard.ShogiUI.Sockets.Managers;
+using Gameboard.ShogiUI.Sockets.Repositories;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
+using System.Security.Claims;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Controllers
{
- [Authorize]
- [ApiController]
- [Route("[controller]")]
- public class GameController : ControllerBase
- {
- private readonly IGameboardRepositoryManager manager;
- private readonly IGameboardRepository repository;
- public GameController(
- IGameboardRepository repository,
- IGameboardRepositoryManager manager)
- {
- this.manager = manager;
- this.repository = repository;
- }
+ [ApiController]
+ [Route("[controller]")]
+ [Authorize(Roles = "Shogi")]
+ public class GameController : ControllerBase
+ {
+ private readonly IGameboardManager gameboardManager;
+ private readonly IGameboardRepository gameboardRepository;
+ private readonly ISocketConnectionManager communicationManager;
- [Route("JoinCode")]
- public async Task PostGameInvitation([FromBody] PostGameInvitation request)
- {
- var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
- var isPlayer1 = await manager.IsPlayer1(request.SessionName, userName);
- if (isPlayer1)
- {
- var code = (await repository.PostJoinCode(request.SessionName, userName)).JoinCode;
- return new CreatedResult("", new PostGameInvitationResponse(code));
- }
- else
- {
- return new UnauthorizedResult();
- }
- }
+ public GameController(
+ IGameboardRepository repository,
+ IGameboardManager manager,
+ ISocketConnectionManager communicationManager)
+ {
+ gameboardManager = manager;
+ gameboardRepository = repository;
+ this.communicationManager = communicationManager;
+ }
- [AllowAnonymous]
- [Route("GuestJoinCode")]
- public async Task PostGuestGameInvitation([FromBody] PostGuestGameInvitation request)
- {
+ [HttpPost("JoinCode")]
+ public async Task PostGameInvitation([FromBody] PostGameInvitation request)
+ {
- var isGuest = manager.IsGuest(request.GuestId);
- var isPlayer1 = manager.IsPlayer1(request.SessionName, request.GuestId);
- if (isGuest && await isPlayer1)
- {
- var code = (await repository.PostJoinCode(request.SessionName, request.GuestId)).JoinCode;
- return new CreatedResult("", new PostGameInvitationResponse(code));
- }
- else
- {
- return new UnauthorizedResult();
- }
- }
- }
+ //var isPlayer1 = await gameboardManager.IsPlayer1(request.SessionName, userName);
+ //if (isPlayer1)
+ //{
+ // var code = await gameboardRepository.PostJoinCode(request.SessionName, userName);
+ // return new CreatedResult("", new PostGameInvitationResponse(code));
+ //}
+ //else
+ //{
+ return new UnauthorizedResult();
+ //}
+ }
+
+ [AllowAnonymous]
+ [HttpPost("GuestJoinCode")]
+ public async Task PostGuestGameInvitation([FromBody] PostGuestGameInvitation request)
+ {
+
+ //var isGuest = gameboardManager.IsGuest(request.GuestId);
+ //var isPlayer1 = gameboardManager.IsPlayer1(request.SessionName, request.GuestId);
+ //if (isGuest && await isPlayer1)
+ //{
+ // var code = await gameboardRepository.PostJoinCode(request.SessionName, request.GuestId);
+ // return new CreatedResult("", new PostGameInvitationResponse(code));
+ //}
+ //else
+ //{
+ return new UnauthorizedResult();
+ //}
+ }
+
+ [HttpPost("{gameName}/Move")]
+ public async Task PostMove([FromRoute] string gameName, [FromBody] PostMove request)
+ {
+ var user = await gameboardManager.ReadUser(User);
+ var session = await gameboardRepository.ReadSession(gameName);
+ if (session == null)
+ {
+ return NotFound();
+ }
+ if (user == null || (session.Player1.Id != user.Id && session.Player2?.Id != user.Id))
+ {
+ return Forbid("User is not seated at this game.");
+ }
+
+ var move = request.Move;
+ var moveModel = move.PieceFromCaptured.HasValue
+ ? new Models.Move(move.PieceFromCaptured.Value, move.To, move.IsPromotion)
+ : new Models.Move(move.From!, move.To, move.IsPromotion);
+ var moveSuccess = session.Shogi.Move(moveModel);
+
+ if (moveSuccess)
+ {
+ var createSuccess = await gameboardRepository.CreateBoardState(session);
+ if (!createSuccess)
+ {
+ throw new ApplicationException("Unable to persist board state.");
+ }
+ await communicationManager.BroadcastToPlayers(new MoveResponse
+ {
+ GameName = session.Name,
+ PlayerName = user.Id
+ }, session.Player1.Id, session.Player2?.Id);
+ return Ok();
+ }
+ return Conflict("Illegal move.");
+ }
+
+ // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too.
+ //[Route("")]
+ //public async Task PostSession([FromBody] PostSession request)
+ //{
+ // var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2);
+ // var success = await repository.CreateSession(model);
+ // if (success)
+ // {
+ // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.ClientAction.CreateGame)
+ // {
+ // Game = model.ToServiceModel(),
+ // PlayerName =
+ // }
+ // var task = request.IsPrivate
+ // ? communicationManager.BroadcastToPlayers(response, userName)
+ // : communicationManager.BroadcastToAll(response);
+ // return new CreatedResult("", null);
+ // }
+ // return new ConflictResult();
+ //}
+
+ [HttpPost]
+ public async Task PostSession([FromBody] PostSession request)
+ {
+ var user = await ReadUserOrThrow();
+ var session = new Models.SessionMetadata(request.Name, request.IsPrivate, user!);
+ var success = await gameboardRepository.CreateSession(session);
+
+ if (success)
+ {
+ try
+ {
+
+ await communicationManager.BroadcastToAll(new CreateGameResponse
+ {
+ Game = session.ToServiceModel(),
+ PlayerName = user.Id
+ });
+ }
+ catch (Exception e)
+ {
+ Console.Error.WriteLine("Error broadcasting during PostSession");
+ }
+
+ return Ok();
+ }
+ return Conflict();
+
+ }
+
+ ///
+ /// Reads the board session and subscribes the caller to socket events for that session.
+ ///
+ [HttpGet("{gameName}")]
+ public async Task GetSession([FromRoute] string gameName)
+ {
+ var user = await ReadUserOrThrow();
+ var session = await gameboardRepository.ReadSession(gameName);
+ if (session == null)
+ {
+ return NotFound();
+ }
+
+ var playerPerspective = WhichPerspective.Spectator;
+ if (session.Player1.Id == user.Id)
+ {
+ playerPerspective = WhichPerspective.Player1;
+ }
+ else if (session.Player2?.Id == user.Id)
+ {
+ playerPerspective = WhichPerspective.Player2;
+ }
+
+ communicationManager.SubscribeToGame(session, user!.Id);
+ var response = new GetSessionResponse()
+ {
+ Game = new Models.SessionMetadata(session).ToServiceModel(),
+ BoardState = session.Shogi.ToServiceModel(),
+ MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(),
+ PlayerPerspective = playerPerspective
+ };
+ return new JsonResult(response);
+ }
+
+ [HttpGet]
+ public async Task GetSessions()
+ {
+ var user = await ReadUserOrThrow();
+ var sessions = await gameboardRepository.ReadSessionMetadatas();
+
+ var sessionsJoinedByUser = sessions
+ .Where(s => s.IsSeated(user))
+ .Select(s => s.ToServiceModel())
+ .ToList();
+ var sessionsNotJoinedByUser = sessions
+ .Where(s => !s.IsSeated(user))
+ .Select(s => s.ToServiceModel())
+ .ToList();
+
+ return new GetSessionsResponse
+ {
+ PlayerHasJoinedSessions = new Collection(sessionsJoinedByUser),
+ AllOtherSessions = new Collection(sessionsNotJoinedByUser)
+ };
+ }
+
+ [HttpPut("{gameName}")]
+ public async Task PutJoinSession([FromRoute] string gameName)
+ {
+ var user = await ReadUserOrThrow();
+ var session = await gameboardRepository.ReadSessionMetaData(gameName);
+ if (session == null)
+ {
+ return NotFound();
+ }
+ if (session.Player2 != null)
+ {
+ return this.Conflict("This session already has two seated players and is full.");
+ }
+
+ session.SetPlayer2(user);
+ var success = await gameboardRepository.UpdateSession(session);
+ if (!success) return this.Problem(detail: "Unable to update session.");
+
+ var opponentName = user.Id == session.Player1.Id
+ ? session.Player2!.Id
+ : session.Player1.Id;
+ await communicationManager.BroadcastToPlayers(new JoinGameResponse
+ {
+ GameName = session.Name,
+ PlayerName = user.Id
+ }, opponentName);
+ return Ok();
+ }
+
+ [Authorize(Roles = "Admin, Shogi")]
+ [HttpDelete("{gameName}")]
+ public async Task DeleteSession([FromRoute] string gameName)
+ {
+ var user = await ReadUserOrThrow();
+ if (user.IsAdmin)
+ {
+ return Ok();
+ }
+ else
+ {
+ return Unauthorized();
+ }
+ }
+
+ private async Task ReadUserOrThrow()
+ {
+ var user = await gameboardManager.ReadUser(User);
+ if (user == null)
+ {
+ throw new UnauthorizedAccessException("Unknown user claims.");
+ }
+ return user;
+ }
+ }
}
diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs
index a729cdb..6f442bd 100644
--- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs
+++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs
@@ -1,61 +1,115 @@
-using Gameboard.ShogiUI.Sockets.Managers;
+using Gameboard.ShogiUI.Sockets.Extensions;
+using Gameboard.ShogiUI.Sockets.Managers;
+using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using System.Linq;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Security.Claims;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Controllers
{
- [Authorize]
- [Route("[controller]")]
- [ApiController]
- public class SocketController : ControllerBase
- {
- private readonly ISocketTokenManager tokenManager;
- private readonly IGameboardRepository gameboardRepository;
- private readonly IGameboardRepositoryManager gameboardManager;
+ [ApiController]
+ [Route("[controller]")]
+ [Authorize(Roles = "Shogi")]
+ public class SocketController : ControllerBase
+ {
+ private readonly ILogger logger;
+ private readonly ISocketTokenCache tokenCache;
+ private readonly IGameboardManager gameboardManager;
+ private readonly IGameboardRepository gameboardRepository;
+ private readonly ISocketConnectionManager connectionManager;
+ private readonly AuthenticationProperties authenticationProps;
- public SocketController(
- ISocketTokenManager tokenManager,
- IGameboardRepository gameboardRepository,
- IGameboardRepositoryManager gameboardManager)
- {
- this.tokenManager = tokenManager;
- this.gameboardRepository = gameboardRepository;
- this.gameboardManager = gameboardManager;
- }
+ public SocketController(
+ ILogger logger,
+ ISocketTokenCache tokenCache,
+ IGameboardManager gameboardManager,
+ IGameboardRepository gameboardRepository,
+ ISocketConnectionManager connectionManager)
+ {
+ this.logger = logger;
+ this.tokenCache = tokenCache;
+ this.gameboardManager = gameboardManager;
+ this.gameboardRepository = gameboardRepository;
+ this.connectionManager = connectionManager;
- [Route("Token")]
- public IActionResult GetToken()
- {
- var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
- var token = tokenManager.GenerateToken(userName);
- return new JsonResult(new GetTokenResponse(token));
- }
+ authenticationProps = new AuthenticationProperties
+ {
+ AllowRefresh = true,
+ IsPersistent = true
+ };
+ }
- [AllowAnonymous]
- [Route("GuestToken")]
- public async Task GetGuestToken([FromQuery] GetGuestToken request)
- {
- if (request.ClientId == null)
- {
- var clientId = await gameboardManager.CreateGuestUser();
- var token = tokenManager.GenerateToken(clientId);
- return new JsonResult(new GetGuestTokenResponse(clientId, token));
- }
- else
- {
- var response = await gameboardRepository.GetPlayer(request.ClientId);
- if (response != null && response.Player != null)
- {
- var token = tokenManager.GenerateToken(response.Player.Name);
- return new JsonResult(new GetGuestTokenResponse(response.Player.Name, token));
- }
- }
- return new UnauthorizedResult();
- }
- }
+ [HttpGet("GuestLogout")]
+ [AllowAnonymous]
+ public async Task GuestLogout()
+ {
+ var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+
+ var userId = User?.UserId();
+ if (!string.IsNullOrEmpty(userId))
+ {
+ connectionManager.UnsubscribeFromBroadcastAndGames(userId);
+ }
+
+ await signoutTask;
+ return Ok();
+ }
+
+ [HttpGet("Token")]
+ public async Task GetToken()
+ {
+ var user = await gameboardManager.ReadUser(User);
+ if (user == null)
+ {
+ if (await gameboardManager.CreateUser(User))
+ {
+ user = await gameboardManager.ReadUser(User);
+ }
+ }
+
+ if (user == null)
+ {
+ return Unauthorized();
+ }
+
+ var token = tokenCache.GenerateToken(user.Id);
+ return new JsonResult(new GetTokenResponse(token));
+ }
+
+ [HttpGet("GuestToken")]
+ [AllowAnonymous]
+ public async Task GetGuestToken()
+ {
+ var user = await gameboardManager.ReadUser(User);
+ if (user == null)
+ {
+ // Create a guest user.
+ var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString());
+ var success = await gameboardRepository.CreateUser(newUser);
+ if (!success)
+ {
+ return Conflict();
+ }
+
+ var identity = newUser.CreateClaimsIdentity();
+ await HttpContext.SignInAsync(
+ CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(identity),
+ authenticationProps
+ );
+ user = newUser;
+ }
+
+ var token = tokenCache.GenerateToken(user.Id.ToString());
+ return this.Ok(new GetGuestTokenResponse(user.Id, user.DisplayName, token));
+ }
+ }
}
diff --git a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
new file mode 100644
index 0000000..ca6dc5c
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
@@ -0,0 +1,28 @@
+using System.Linq;
+using System.Security.Claims;
+
+namespace Gameboard.ShogiUI.Sockets.Extensions
+{
+ public static class Extensions
+ {
+ public static string? UserId(this ClaimsPrincipal self)
+ {
+ return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
+ }
+
+ public static string? DisplayName(this ClaimsPrincipal self)
+ {
+ return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
+ }
+
+ public static bool IsGuest(this ClaimsPrincipal self)
+ {
+ return self.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Guest");
+ }
+
+ public static string ToCamelCase(this string self)
+ {
+ return char.ToLowerInvariant(self[0]) + self[1..];
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs b/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs
index 3c39341..d4860c4 100644
--- a/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs
+++ b/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs
@@ -1,43 +1,50 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
+using System.IO;
+using System.Text;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Extensions
{
- public class LogMiddleware
- {
- private readonly RequestDelegate next;
- private readonly ILogger logger;
+ public class LogMiddleware
+ {
+ private readonly RequestDelegate next;
+ private readonly ILogger logger;
- public LogMiddleware(RequestDelegate next, ILoggerFactory factory)
- {
- this.next = next;
- logger = factory.CreateLogger();
- }
- public async Task Invoke(HttpContext context)
- {
- try
- {
- await next(context);
- }
- finally
- {
- logger.LogInformation("Request {method} {url} => {statusCode}",
- context.Request?.Method,
- context.Request?.Path.Value,
- context.Response?.StatusCode);
- }
- }
- }
+ public LogMiddleware(RequestDelegate next, ILoggerFactory factory)
+ {
+ this.next = next;
+ logger = factory.CreateLogger();
+ }
- public static class IApplicationBuilderExtensions
- {
- public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder)
- {
- builder.UseMiddleware();
- return builder;
- }
- }
+ public async Task Invoke(HttpContext context)
+ {
+ try
+ {
+ await next(context);
+ }
+ finally
+ {
+ using var stream = new MemoryStream();
+ context.Request?.Body.CopyToAsync(stream);
+
+ logger.LogInformation("Request {method} {url} => {statusCode} \n Body: {body}",
+ context.Request?.Method,
+ context.Request?.Path.Value,
+ context.Response?.StatusCode,
+ Encoding.UTF8.GetString(stream.ToArray()));
+ }
+ }
+ }
+
+ public static class IApplicationBuilderExtensions
+ {
+ public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder)
+ {
+ builder.UseMiddleware();
+ return builder;
+ }
+ }
}
diff --git a/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs
new file mode 100644
index 0000000..d5333f4
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs
@@ -0,0 +1,63 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Gameboard.ShogiUI.Sockets.Extensions
+{
+ public static class ModelExtensions
+ {
+ public static string GetShortName(this Models.Piece self)
+ {
+ var name = self.WhichPiece switch
+ {
+ WhichPiece.King => " K ",
+ WhichPiece.GoldGeneral => " G ",
+ WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ",
+ WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ",
+ WhichPiece.Rook => self.IsPromoted ? "^R " : " R ",
+ WhichPiece.Knight => self.IsPromoted ? "^k " : " k ",
+ WhichPiece.Lance => self.IsPromoted ? "^L " : " L ",
+ WhichPiece.Pawn => self.IsPromoted ? "^P " : " P ",
+ _ => " ? ",
+ };
+ if (self.Owner == WhichPerspective.Player2)
+ name = Regex.Replace(name, @"([^\s]+)\s", "$1.");
+ return name;
+ }
+
+ public static string PrintStateAsAscii(this Models.Shogi self)
+ {
+ var builder = new StringBuilder();
+ builder.Append(" Player 2(.)");
+ builder.AppendLine();
+ for (var y = 8; y >= 0; y--)
+ {
+ builder.Append("- ");
+ for (var x = 0; x < 8; x++) builder.Append("- - ");
+ builder.Append("- -");
+ builder.AppendLine();
+ builder.Append('|');
+ for (var x = 0; x < 9; x++)
+ {
+ var piece = self.Board[x, y];
+ if (piece == null)
+ {
+ builder.Append(" ");
+ }
+ else
+ {
+ builder.AppendFormat("{0}", piece.GetShortName());
+ }
+ builder.Append('|');
+ }
+ builder.AppendLine();
+ }
+ builder.Append("- ");
+ for (var x = 0; x < 8; x++) builder.Append("- - ");
+ builder.Append("- -");
+ builder.AppendLine();
+ builder.Append(" Player 1");
+ return builder.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj
index 3f3669b..e05ef31 100644
--- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj
+++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj
@@ -1,21 +1,27 @@
- net5.0
+ net6.0
+ true
+ 5
+ enable
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user
deleted file mode 100644
index 2adf92b..0000000
--- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- ApiControllerEmptyScaffolder
- root/Controller
- AspShogiSockets
- false
-
-
- ProjectDebugger
-
-
\ No newline at end of file
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs
deleted file mode 100644
index b7e3773..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using Gameboard.Shogi.Api.ServiceModels.Messages;
-using Gameboard.ShogiUI.Sockets.Extensions;
-using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using System.Net.WebSockets;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
-{
- public class CreateGameHandler : IActionHandler
- {
- private readonly ILogger logger;
- private readonly IGameboardRepository repository;
- private readonly ISocketCommunicationManager communicationManager;
-
- public CreateGameHandler(
- ILogger logger,
- ISocketCommunicationManager communicationManager,
- IGameboardRepository repository)
- {
- this.logger = logger;
- this.repository = repository;
- this.communicationManager = communicationManager;
- }
-
- public async Task Handle(WebSocket socket, string json, string userName)
- {
- logger.LogInformation("Socket Request \n{0}\n", new[] { json });
- var request = JsonConvert.DeserializeObject(json);
- var postSessionResponse = await repository.PostSession(new PostSession
- {
- SessionName = request.GameName,
- PlayerName = userName, // TODO : Investigate if needed by UI
- IsPrivate = request.IsPrivate
- });
-
- var response = new CreateGameResponse(request.Action)
- {
- PlayerName = userName,
- Game = new Game
- {
- GameName = postSessionResponse.SessionName,
- Players = new string[] { userName }
- }
- };
-
- if (string.IsNullOrWhiteSpace(postSessionResponse.SessionName))
- {
- response.Error = "Game already exists.";
- }
-
- var serialized = JsonConvert.SerializeObject(response);
- logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
- if (request.IsPrivate)
- {
- await socket.SendTextAsync(serialized);
- }
- else
- {
- await communicationManager.BroadcastToAll(serialized);
- }
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs
deleted file mode 100644
index 5168598..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using System.Net.WebSockets;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
-{
- public interface IActionHandler
- {
- ///
- /// Responsible for parsing json and handling the request.
- ///
- Task Handle(WebSocket socket, string json, string userName);
- }
-
- public delegate IActionHandler ActionHandlerResolver(ClientAction action);
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs
index 102564a..2dcac05 100644
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs
+++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs
@@ -1,75 +1,64 @@
-using Gameboard.Shogi.Api.ServiceModels.Messages;
-using Gameboard.ShogiUI.Sockets.Extensions;
-using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using System.Net.WebSockets;
+using Gameboard.ShogiUI.Sockets.Repositories;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using System.Threading.Tasks;
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 ILogger logger;
private readonly IGameboardRepository repository;
- private readonly ISocketCommunicationManager communicationManager;
+ private readonly ISocketConnectionManager communicationManager;
public JoinByCodeHandler(
- ILogger logger,
- ISocketCommunicationManager communicationManager,
+ ISocketConnectionManager communicationManager,
IGameboardRepository repository)
{
- this.logger = logger;
this.repository = repository;
this.communicationManager = communicationManager;
}
- public async Task Handle(WebSocket socket, string json, string userName)
+ public async Task Handle(JoinByCodeRequest request, string userName)
{
- logger.LogInformation("Socket Request \n{0}\n", new[] { json });
- var request = JsonConvert.DeserializeObject(json);
- var joinGameResponse = await repository.PostJoinPrivateSession(new PostJoinPrivateSession
- {
- PlayerName = userName,
- JoinCode = request.JoinCode
- });
+ //var request = JsonConvert.DeserializeObject(json);
+ //var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession
+ //{
+ // PlayerName = userName,
+ // JoinCode = request.JoinCode
+ //});
- if (joinGameResponse.JoinSucceeded)
- {
- var gameName = (await repository.GetGame(joinGameResponse.SessionName)).Session.Name;
+ //if (sessionName == null)
+ //{
+ // var response = new JoinGameResponse(ClientAction.JoinByCode)
+ // {
+ // PlayerName = userName,
+ // GameName = sessionName,
+ // Error = "Error joining game."
+ // };
+ // await communicationManager.BroadcastToPlayers(response, userName);
+ //}
+ //else
+ //{
+ // // Other members of the game see a regular JoinGame occur.
+ // var response = new JoinGameResponse(ClientAction.JoinGame)
+ // {
+ // PlayerName = userName,
+ // GameName = sessionName
+ // };
+ // // At this time, userName hasn't subscribed and won't receive this message.
+ // await communicationManager.BroadcastToGame(sessionName, response);
- // Other members of the game see a regular JoinGame occur.
- var response = new JoinGameResponse(ClientAction.JoinGame)
- {
- PlayerName = userName,
- GameName = gameName
- };
- var serialized = JsonConvert.SerializeObject(response);
- await communicationManager.BroadcastToGame(gameName, serialized);
- communicationManager.SubscribeToGame(socket, gameName, userName);
-
- // But the player joining sees the JoinByCode occur.
- response = new JoinGameResponse(ClientAction.JoinByCode)
- {
- PlayerName = userName,
- GameName = gameName
- };
- serialized = JsonConvert.SerializeObject(response);
- await socket.SendTextAsync(serialized);
- }
- else
- {
- var response = new JoinGameResponse(ClientAction.JoinByCode)
- {
- PlayerName = userName,
- Error = "Error joining game."
- };
- var serialized = JsonConvert.SerializeObject(response);
- logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
- await socket.SendTextAsync(serialized);
- }
+ // // The player joining sees the JoinByCode occur.
+ // response = new JoinGameResponse(ClientAction.JoinByCode)
+ // {
+ // PlayerName = userName,
+ // GameName = sessionName
+ // };
+ // await communicationManager.BroadcastToPlayers(response, userName);
+ //}
}
}
}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs
deleted file mode 100644
index c435d63..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using Gameboard.Shogi.Api.ServiceModels.Messages;
-using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using System.Net.WebSockets;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
-{
- public class JoinGameHandler : IActionHandler
- {
- private readonly ILogger logger;
- private readonly IGameboardRepository gameboardRepository;
- private readonly ISocketCommunicationManager communicationManager;
- public JoinGameHandler(
- ILogger logger,
- ISocketCommunicationManager communicationManager,
- IGameboardRepository gameboardRepository)
- {
- this.logger = logger;
- this.gameboardRepository = gameboardRepository;
- this.communicationManager = communicationManager;
- }
-
- public async Task Handle(WebSocket socket, string json, string userName)
- {
- logger.LogInformation("Socket Request \n{0}\n", new[] { json });
- var request = JsonConvert.DeserializeObject(json);
- var response = new JoinGameResponse(ClientAction.JoinGame)
- {
- PlayerName = userName
- };
-
- var joinGameResponse = await gameboardRepository.PutJoinPublicSession(request.GameName, new PutJoinPublicSession
- {
- PlayerName = userName,
- SessionName = request.GameName
- });
-
- if (joinGameResponse.JoinSucceeded)
- {
- response.GameName = request.GameName;
- }
- else
- {
- response.Error = "Game is full or code is incorrect.";
- }
- var serialized = JsonConvert.SerializeObject(response);
- logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
- await communicationManager.BroadcastToAll(serialized);
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs
deleted file mode 100644
index 72050fc..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using Gameboard.ShogiUI.Sockets.Extensions;
-using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using System;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
-{
- public class ListGamesHandler : IActionHandler
- {
- private readonly ILogger logger;
- private readonly IGameboardRepository repository;
-
- public ListGamesHandler(
- ILogger logger,
- IGameboardRepository repository)
- {
- this.logger = logger;
- this.repository = repository;
- }
-
- public async Task Handle(WebSocket socket, string json, string userName)
- {
- logger.LogInformation("Socket Request \n{0}\n", new[] { json });
- var request = JsonConvert.DeserializeObject(json);
- var getGamesResponse = string.IsNullOrWhiteSpace(userName)
- ? await repository.GetGames()
- : await repository.GetGames(userName);
-
- var games = getGamesResponse.Sessions
- .OrderBy(s => s.Player1 == userName || s.Player2 == userName)
- .Select(s =>
- {
- var players = new[] { s.Player1, s.Player2 }
- .Where(p => !string.IsNullOrWhiteSpace(p))
- .ToArray();
- return new Game { GameName = s.Name, Players = players };
- });
- var response = new ListGamesResponse(ClientAction.ListGames)
- {
- Games = games ?? Array.Empty()
- };
-
- var serialized = JsonConvert.SerializeObject(response);
- logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
- await socket.SendTextAsync(serialized);
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs
deleted file mode 100644
index 97e7c32..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using Gameboard.ShogiUI.Sockets.Extensions;
-using Gameboard.ShogiUI.Sockets.Managers.Utility;
-using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
-{
- public class LoadGameHandler : IActionHandler
- {
- private readonly ILogger logger;
- private readonly IGameboardRepository gameboardRepository;
- private readonly ISocketCommunicationManager communicationManager;
-
- public LoadGameHandler(
- ILogger logger,
- ISocketCommunicationManager communicationManager,
- IGameboardRepository gameboardRepository)
- {
- this.logger = logger;
- this.gameboardRepository = gameboardRepository;
- this.communicationManager = communicationManager;
- }
-
- public async Task Handle(WebSocket socket, string json, string userName)
- {
- logger.LogInformation("Socket Request \n{0}\n", json);
- var request = JsonConvert.DeserializeObject(json);
- var response = new LoadGameResponse(ClientAction.LoadGame);
- var getGameResponse = await gameboardRepository.GetGame(request.GameName);
- var getMovesResponse = await gameboardRepository.GetMoves(request.GameName);
-
- if (getGameResponse == null || getMovesResponse == null)
- {
- response.Error = $"Could not find game.";
- }
- else
- {
- var session = getGameResponse.Session;
- var players = new[] { session.Player1, session.Player2 }
- .Where(p => !string.IsNullOrWhiteSpace(p))
- .ToArray();
- response.Game = new Game { GameName = session.Name, Players = players };
-
- response.Moves = userName.Equals(session.Player1)
- ? getMovesResponse.Moves.Select(_ => Mapper.Map(_))
- : getMovesResponse.Moves.Select(_ => Move.ConvertPerspective(Mapper.Map(_)));
-
- communicationManager.SubscribeToGame(socket, session.Name, userName);
- }
-
- var serialized = JsonConvert.SerializeObject(response);
- logger.LogInformation("Socket Response \n{0}\n", serialized);
- await socket.SendTextAsync(serialized);
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs
deleted file mode 100644
index bbce0c5..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using Gameboard.Shogi.Api.ServiceModels.Messages;
-using Gameboard.ShogiUI.Sockets.Extensions;
-using Gameboard.ShogiUI.Sockets.Managers.Utility;
-using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Messages;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using System.Net.WebSockets;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
-{
- public class MoveHandler : IActionHandler
- {
- private readonly ILogger logger;
- private readonly IGameboardRepository gameboardRepository;
- private readonly ISocketCommunicationManager communicationManager;
- public MoveHandler(
- ILogger logger,
- ISocketCommunicationManager communicationManager,
- IGameboardRepository gameboardRepository)
- {
- this.logger = logger;
- this.gameboardRepository = gameboardRepository;
- this.communicationManager = communicationManager;
- }
-
- public async Task Handle(WebSocket socket, string json, string userName)
- {
- logger.LogInformation("Socket Request \n{0}\n", new[] { json });
- var request = JsonConvert.DeserializeObject(json);
- // Basic move validation
- var move = request.Move;
- if (move.To.Equals(move.From))
- {
- var serialized = JsonConvert.SerializeObject(
- new ErrorResponse(ClientAction.Move)
- {
- Error = "Error: moving piece from tile to the same tile."
- });
- await socket.SendTextAsync(serialized);
- return;
- }
-
- var getSessionResponse = await gameboardRepository.GetGame(request.GameName);
- var isPlayer1 = userName == getSessionResponse.Session.Player1;
- if (!isPlayer1)
- {
- // Convert the move coords to player1 perspective.
- move = Move.ConvertPerspective(move);
- }
-
- await gameboardRepository.PostMove(request.GameName, new PostMove(Mapper.Map(move)));
-
- var response = new MoveResponse(ClientAction.Move)
- {
- GameName = request.GameName,
- PlayerName = userName
- };
- await communicationManager.BroadcastToGame(
- request.GameName,
- (playerName, sslStream) =>
- {
- response.Move = playerName.Equals(userName)
- ? request.Move
- : Move.ConvertPerspective(request.Move);
- var serialized = JsonConvert.SerializeObject(response);
- logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
- return serialized;
- }
- );
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs
new file mode 100644
index 0000000..5cc9f04
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs
@@ -0,0 +1,74 @@
+using Gameboard.ShogiUI.Sockets.Extensions;
+using Gameboard.ShogiUI.Sockets.Models;
+using Gameboard.ShogiUI.Sockets.Repositories;
+using System;
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace Gameboard.ShogiUI.Sockets.Managers
+{
+ public interface IGameboardManager
+ {
+ Task AssignPlayer2ToSession(string sessionName, User user);
+ Task ReadUser(ClaimsPrincipal user);
+ Task CreateUser(ClaimsPrincipal user);
+ }
+
+ public class GameboardManager : IGameboardManager
+ {
+ private readonly IGameboardRepository repository;
+
+ public GameboardManager(IGameboardRepository repository)
+ {
+ this.repository = repository;
+ }
+
+ public Task CreateUser(ClaimsPrincipal principal)
+ {
+ var id = principal.UserId();
+ if (string.IsNullOrEmpty(id))
+ {
+ return Task.FromResult(false);
+ }
+
+ var user = principal.IsGuest()
+ ? User.CreateGuestUser(id)
+ : User.CreateMsalUser(id);
+
+ return repository.CreateUser(user);
+ }
+
+ public Task ReadUser(ClaimsPrincipal principal)
+ {
+ var userId = principal.UserId();
+ if (!string.IsNullOrEmpty(userId))
+ {
+ return repository.ReadUser(userId);
+ }
+
+ return Task.FromResult(null);
+ }
+
+
+ public async Task CreateJoinCode(string sessionName, string playerName)
+ {
+ //var session = await repository.GetGame(sessionName);
+ //if (playerName == session?.Player1)
+ //{
+ // return await repository.PostJoinCode(sessionName, playerName);
+ //}
+ return string.Empty;
+ }
+
+ public async Task AssignPlayer2ToSession(string sessionName, User user)
+ {
+ var session = await repository.ReadSessionMetaData(sessionName);
+ if (session != null && !session.IsPrivate && session.Player2 == null)
+ {
+ session.SetPlayer2(user);
+ return await repository.UpdateSession(session);
+ }
+ return false;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs
deleted file mode 100644
index 8e68a7a..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs
+++ /dev/null
@@ -1,172 +0,0 @@
-using Gameboard.ShogiUI.Sockets.Extensions;
-using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
-using Gameboard.ShogiUI.Sockets.Managers.Utility;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Managers
-{
- public interface ISocketCommunicationManager
- {
- Task CommunicateWith(WebSocket w, string s);
- Task BroadcastToAll(string msg);
- Task BroadcastToGame(string gameName, Func msgBuilder);
- Task BroadcastToGame(string gameName, string msg);
- void SubscribeToGame(WebSocket socket, string gameName, string playerName);
- void SubscribeToBroadcast(WebSocket socket, string playerName);
- void UnsubscribeFromBroadcastAndGames(string playerName);
- void UnsubscribeFromGame(string gameName, string playerName);
- }
-
- public class SocketCommunicationManager : ISocketCommunicationManager
- {
- private readonly ConcurrentDictionary connections;
- private readonly ConcurrentDictionary> gameSeats;
- private readonly ILogger logger;
- private readonly ActionHandlerResolver handlerResolver;
-
- public SocketCommunicationManager(
- ILogger logger,
- ActionHandlerResolver handlerResolver)
- {
- this.logger = logger;
- this.handlerResolver = handlerResolver;
- connections = new ConcurrentDictionary();
- gameSeats = new ConcurrentDictionary>();
- }
-
- 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;
-
- var request = JsonConvert.DeserializeObject(message);
- if (!Enum.IsDefined(typeof(ClientAction), request.Action))
- {
- await socket.SendTextAsync("Error: Action not recognized.");
- }
- else
- {
- var handler = handlerResolver(request.Action);
- await handler.Handle(socket, 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)
- {
- logger.LogInformation("Subscribing [{0}] to broadcast", playerName);
- connections.TryAdd(playerName, socket);
- }
-
- public void UnsubscribeFromBroadcastAndGames(string playerName)
- {
- logger.LogInformation("Unsubscribing [{0}] from broadcast", playerName);
- connections.TryRemove(playerName, out _);
- foreach (var game in gameSeats)
- {
- game.Value.Remove(playerName);
- }
- }
-
- ///
- /// Unsubscribes the player from their current game, then subscribes to the new game.
- ///
- public void SubscribeToGame(WebSocket socket, string gameName, string playerName)
- {
- // Unsubscribe from any other games
- foreach (var kvp in gameSeats)
- {
- var gameNameKey = kvp.Key;
- UnsubscribeFromGame(gameNameKey, playerName);
- }
-
- // Subscribe
- logger.LogInformation("Subscribing player [{0}] to game [{1}]", playerName, gameName);
- var addSuccess = gameSeats.TryAdd(gameName, new List { playerName });
- if (!addSuccess && !gameSeats[gameName].Contains(playerName))
- {
- gameSeats[gameName].Add(playerName);
- }
- }
-
- public void UnsubscribeFromGame(string gameName, string playerName)
- {
- if (gameSeats.ContainsKey(gameName))
- {
- logger.LogInformation("Unsubscribing player [{0}] from game [{1}]", playerName, gameName);
- gameSeats[gameName].Remove(playerName);
- if (gameSeats[gameName].Count == 0) gameSeats.TryRemove(gameName, out _);
- }
- }
-
- public async Task BroadcastToAll(string msg)
- {
- var tasks = connections.Select(kvp =>
- {
- var player = kvp.Key;
- var socket = kvp.Value;
- logger.LogInformation("Broadcasting to player [{0}] \n{1}\n", new[] { player, msg });
- return socket.SendTextAsync(msg);
- });
- await Task.WhenAll(tasks);
- }
-
- public async Task BroadcastToGame(string gameName, string msg)
- {
- if (gameSeats.ContainsKey(gameName))
- {
- var tasks = gameSeats[gameName]
- .Select(playerName =>
- {
- logger.LogInformation("Broadcasting to game [{0}], player [{0}] \n{1}\n", gameName, playerName, msg);
- return connections[playerName];
- })
- .Where(stream => stream != null)
- .Select(socket => socket.SendTextAsync(msg));
- await Task.WhenAll(tasks);
- }
- }
-
- public async Task BroadcastToGame(string gameName, Func msgBuilder)
- {
- if (gameSeats.ContainsKey(gameName))
- {
- var tasks = gameSeats[gameName]
- .Select(playerName =>
- {
- var socket = connections[playerName];
- var msg = msgBuilder(playerName, socket);
- logger.LogInformation("Broadcasting to game [{0}], player [{0}] \n{1}\n", gameName, playerName, msg);
- return socket.SendTextAsync(msg);
- });
- await Task.WhenAll(tasks);
- }
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs
index 0e5d729..0b2a5b8 100644
--- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs
+++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs
@@ -1,44 +1,162 @@
-using Microsoft.AspNetCore.Http;
+using Gameboard.ShogiUI.Sockets.Extensions;
+using Gameboard.ShogiUI.Sockets.Models;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
using System;
-using System.Net;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Net.WebSockets;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Managers
{
- public interface ISocketConnectionManager
- {
- Task HandleSocketRequest(HttpContext context);
- }
+ public interface ISocketConnectionManager
+ {
+ Task BroadcastToAll(IResponse response);
+ //Task BroadcastToGame(string gameName, IResponse response);
+ //Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2);
+ void SubscribeToGame(Session session, string playerName);
+ void SubscribeToBroadcast(WebSocket socket, string playerName);
+ void UnsubscribeFromBroadcastAndGames(string playerName);
+ void UnsubscribeFromGame(string gameName, string playerName);
+ Task BroadcastToPlayers(IResponse response, params string?[] playerNames);
+ }
- public class SocketConnectionManager : ISocketConnectionManager
- {
- private readonly ISocketCommunicationManager communicationManager;
- private readonly ISocketTokenManager tokenManager;
+ ///
+ /// Retains all active socket connections and provides convenient methods for sending messages to clients.
+ ///
+ public class SocketConnectionManager : ISocketConnectionManager
+ {
+ /// Dictionary key is player name.
+ private readonly ConcurrentDictionary connections;
+ /// Dictionary key is game name.
+ private readonly ConcurrentDictionary sessions;
+ private readonly ILogger logger;
- public SocketConnectionManager(ISocketCommunicationManager communicationManager, ISocketTokenManager tokenManager) : base()
- {
- this.communicationManager = communicationManager;
- this.tokenManager = tokenManager;
+ public SocketConnectionManager(ILogger logger)
+ {
+ this.logger = logger;
+ connections = new ConcurrentDictionary();
+ sessions = new ConcurrentDictionary();
+ }
- }
+ public void SubscribeToBroadcast(WebSocket socket, string playerName)
+ {
+ connections.TryRemove(playerName, out var _);
+ connections.TryAdd(playerName, socket);
+ }
- public async Task HandleSocketRequest(HttpContext context)
- {
- var hasToken = context.Request.Query.Keys.Contains("token");
- if (hasToken)
- {
- var oneTimeToken = context.Request.Query["token"][0];
- var tokenAsGuid = Guid.Parse(oneTimeToken);
- var userName = tokenManager.GetUsername(tokenAsGuid);
- if (!string.IsNullOrEmpty(userName))
- {
- var socket = await context.WebSockets.AcceptWebSocketAsync();
- await communicationManager.CommunicateWith(socket, userName);
- return;
- }
- }
- context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
- return;
- }
- }
+ public void UnsubscribeFromBroadcastAndGames(string playerName)
+ {
+ connections.TryRemove(playerName, out _);
+ foreach (var kvp in sessions)
+ {
+ var sessionName = kvp.Key;
+ UnsubscribeFromGame(sessionName, playerName);
+ }
+ }
+
+ ///
+ /// Unsubscribes the player from their current game, then subscribes to the new game.
+ ///
+ public void SubscribeToGame(Session session, string playerName)
+ {
+ // Unsubscribe from any other games
+ foreach (var kvp in sessions)
+ {
+ var gameNameKey = kvp.Key;
+ UnsubscribeFromGame(gameNameKey, playerName);
+ }
+
+ // Subscribe
+ if (connections.TryGetValue(playerName, out var socket))
+ {
+ var s = sessions.GetOrAdd(session.Name, session);
+ s.Subscriptions.TryAdd(playerName, socket);
+ }
+ }
+
+ public void UnsubscribeFromGame(string gameName, string playerName)
+ {
+ if (sessions.TryGetValue(gameName, out var s))
+ {
+ s.Subscriptions.TryRemove(playerName, out _);
+ if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _);
+ }
+ }
+
+ public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames)
+ {
+ var tasks = new List(playerNames.Length);
+ foreach (var name in playerNames)
+ {
+ if (!string.IsNullOrEmpty(name) && connections.TryGetValue(name, out var socket))
+ {
+ var serialized = JsonConvert.SerializeObject(response);
+ logger.LogInformation("Response to {0} \n{1}\n", name, serialized);
+ tasks.Add(socket.SendTextAsync(serialized));
+ }
+ }
+ await Task.WhenAll(tasks);
+ }
+ public Task BroadcastToAll(IResponse response)
+ {
+ var message = JsonConvert.SerializeObject(response);
+ logger.LogInformation($"Broadcasting\n{0}", message);
+ var tasks = new List(connections.Count);
+ foreach (var kvp in connections)
+ {
+ var socket = kvp.Value;
+ try
+ {
+
+ tasks.Add(socket.SendTextAsync(message));
+ }
+ catch (WebSocketException webSocketException)
+ {
+ logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
+ UnsubscribeFromBroadcastAndGames(kvp.Key);
+ }
+ catch (Exception exception)
+ {
+ logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
+ UnsubscribeFromBroadcastAndGames(kvp.Key);
+ }
+ }
+ try
+ {
+ var task = Task.WhenAll(tasks);
+ return task;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine("Yo");
+ }
+ return Task.FromResult(0);
+ }
+
+ //public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2)
+ //{
+ // if (sessions.TryGetValue(gameName, out var session))
+ // {
+ // var serialized1 = JsonConvert.SerializeObject(forPlayer1);
+ // var serialized2 = JsonConvert.SerializeObject(forPlayer2);
+ // return Task.WhenAll(
+ // session.SendToPlayer1(serialized1),
+ // session.SendToPlayer2(serialized2));
+ // }
+ // return Task.CompletedTask;
+ //}
+
+ //public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers)
+ //{
+ // if (sessions.TryGetValue(gameName, out var session))
+ // {
+ // var serialized = JsonConvert.SerializeObject(messageForAllPlayers);
+ // return session.Broadcast(serialized);
+ // }
+ // return Task.CompletedTask;
+ //}
+ }
}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs
new file mode 100644
index 0000000..722dc33
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Gameboard.ShogiUI.Sockets.Managers
+{
+ public interface ISocketTokenCache
+ {
+ Guid GenerateToken(string s);
+ string? GetUsername(Guid g);
+ }
+
+ public class SocketTokenCache : ISocketTokenCache
+ {
+ ///
+ /// Key is userName or webSessionId
+ ///
+ private readonly ConcurrentDictionary Tokens;
+
+ public SocketTokenCache()
+ {
+ Tokens = new ConcurrentDictionary();
+ }
+
+ public Guid GenerateToken(string userName)
+ {
+ Tokens.Remove(userName, out _);
+
+ var guid = Guid.NewGuid();
+ Tokens.TryAdd(userName, guid);
+
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromMinutes(1));
+ Tokens.Remove(userName, out _);
+ });
+
+ return guid;
+ }
+
+ /// User name associated to the guid or null.
+ public string? GetUsername(Guid guid)
+ {
+ var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key;
+ if (userName != null)
+ {
+ Tokens.Remove(userName, out _);
+ }
+ return userName;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs
deleted file mode 100644
index 971e438..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Managers
-{
- public interface ISocketTokenManager
- {
- Guid GenerateToken(string s);
- string GetUsername(Guid g);
- }
-
- public class SocketTokenManager : ISocketTokenManager
- {
- ///
- /// Key is userName
- ///
- private readonly Dictionary Tokens;
-
- public SocketTokenManager()
- {
- Tokens = new Dictionary();
- }
-
- public Guid GenerateToken(string userName)
- {
- var guid = Guid.NewGuid();
-
- if (Tokens.ContainsKey(userName))
- {
- Tokens.Remove(userName);
- }
- Tokens.Add(userName, guid);
-
- _ = Task.Run(async () =>
- {
- await Task.Delay(TimeSpan.FromMinutes(1));
- Tokens.Remove(userName);
- });
-
- return guid;
- }
-
- /// User name associated to the guid or null.
- public string GetUsername(Guid guid)
- {
- if (Tokens.ContainsValue(guid))
- {
- var username = Tokens.First(kvp => kvp.Value == guid).Key;
- Tokens.Remove(username);
- return username;
- }
- return null;
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs
deleted file mode 100644
index 9d84fac..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-using Microsoft.FSharp.Core;
-using GameboardTypes = Gameboard.Shogi.Api.ServiceModels.Types;
-
-namespace Gameboard.ShogiUI.Sockets.Managers.Utility
-{
- public static class Mapper
- {
- public static GameboardTypes.Move Map(Move source)
- {
- var from = source.From;
- var to = source.To;
- FSharpOption pieceFromCaptured = source.PieceFromCaptured switch
- {
- "B" => new FSharpOption(GameboardTypes.PieceName.Bishop),
- "G" => new FSharpOption(GameboardTypes.PieceName.GoldenGeneral),
- "K" => new FSharpOption(GameboardTypes.PieceName.King),
- "k" => new FSharpOption(GameboardTypes.PieceName.Knight),
- "L" => new FSharpOption(GameboardTypes.PieceName.Lance),
- "P" => new FSharpOption(GameboardTypes.PieceName.Pawn),
- "R" => new FSharpOption(GameboardTypes.PieceName.Rook),
- "S" => new FSharpOption(GameboardTypes.PieceName.SilverGeneral),
- _ => null
- };
- var target = new GameboardTypes.Move
- {
- Origin = new GameboardTypes.BoardLocation { X = from.X, Y = from.Y },
- Destination = new GameboardTypes.BoardLocation { X = to.X, Y = to.Y },
- IsPromotion = source.IsPromotion,
- PieceFromCaptured = pieceFromCaptured
- };
- return target;
- }
-
- public static Move Map(GameboardTypes.Move source)
- {
- var origin = source.Origin;
- var destination = source.Destination;
- string pieceFromCaptured = null;
- if (source.PieceFromCaptured != null)
- {
- pieceFromCaptured = source.PieceFromCaptured.Value switch
- {
- GameboardTypes.PieceName.Bishop => "B",
- GameboardTypes.PieceName.GoldenGeneral => "G",
- GameboardTypes.PieceName.King => "K",
- GameboardTypes.PieceName.Knight => "k",
- GameboardTypes.PieceName.Lance => "L",
- GameboardTypes.PieceName.Pawn => "P",
- GameboardTypes.PieceName.Rook => "R",
- GameboardTypes.PieceName.SilverGeneral => "S",
- _ => ""
- };
- }
-
- var target = new Move
- {
- From = new Coords { X = origin.X, Y = origin.Y },
- To = new Coords { X = destination.X, Y = destination.Y },
- IsPromotion = source.IsPromotion,
- PieceFromCaptured = pieceFromCaptured
- };
-
- return target;
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs
deleted file mode 100644
index df3f245..0000000
--- a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
-
-namespace Gameboard.ShogiUI.Sockets.Managers.Utility
-{
- public class Request : IRequest
- {
- public ClientAction Action { get; set; }
- public string PlayerName { get; set; }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Models/Move.cs b/Gameboard.ShogiUI.Sockets/Models/Move.cs
new file mode 100644
index 0000000..bbb32f7
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/Move.cs
@@ -0,0 +1,63 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using Gameboard.ShogiUI.Sockets.Utilities;
+using System.Diagnostics;
+using System.Numerics;
+
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ [DebuggerDisplay("{From} - {To}")]
+ public class Move
+ {
+ public Vector2? From { get; } // TODO: Use string notation
+ public bool IsPromotion { get; }
+ public WhichPiece? PieceFromHand { get; }
+ public Vector2 To { get; }
+
+ public Move(Vector2 from, Vector2 to, bool isPromotion = false)
+ {
+ From = from;
+ To = to;
+ IsPromotion = isPromotion;
+ }
+ public Move(WhichPiece pieceFromHand, Vector2 to)
+ {
+ PieceFromHand = pieceFromHand;
+ To = to;
+ }
+
+ ///
+ /// Constructor to represent moving a piece on the Board to another position on the Board.
+ ///
+ /// Position the piece is being moved from.
+ /// Position the piece is being moved to.
+ /// If the moving piece should be promoted.
+ public Move(string fromNotation, string toNotation, bool isPromotion = false)
+ {
+ From = NotationHelper.FromBoardNotation(fromNotation);
+ To = NotationHelper.FromBoardNotation(toNotation);
+ IsPromotion = isPromotion;
+ }
+
+ ///
+ /// Constructor to represent moving a piece from the Hand to the Board.
+ ///
+ /// The piece being moved from the Hand to the Board.
+ /// Position the piece is being moved to.
+ /// If the moving piece should be promoted.
+ public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false)
+ {
+ From = null;
+ PieceFromHand = pieceFromHand;
+ To = NotationHelper.FromBoardNotation(toNotation);
+ IsPromotion = isPromotion;
+ }
+
+ public ServiceModels.Types.Move ToServiceModel() => new()
+ {
+ From = From.HasValue ? NotationHelper.ToBoardNotation(From.Value) : null,
+ IsPromotion = IsPromotion,
+ PieceFromCaptured = PieceFromHand.HasValue ? PieceFromHand : null,
+ To = NotationHelper.ToBoardNotation(To)
+ };
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs b/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs
new file mode 100644
index 0000000..5f4ee8e
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/MoveSets.cs
@@ -0,0 +1,95 @@
+using PathFinding;
+using System.Collections.Generic;
+
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ public static class MoveSets
+ {
+ public static readonly List King = new(8)
+ {
+ new PathFinding.Move(Direction.Up),
+ new PathFinding.Move(Direction.Left),
+ new PathFinding.Move(Direction.Right),
+ new PathFinding.Move(Direction.Down),
+ new PathFinding.Move(Direction.UpLeft),
+ new PathFinding.Move(Direction.UpRight),
+ new PathFinding.Move(Direction.DownLeft),
+ new PathFinding.Move(Direction.DownRight)
+ };
+
+ public static readonly List Bishop = new(4)
+ {
+ new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
+ new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
+ new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
+ new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
+ };
+
+ public static readonly List PromotedBishop = new(8)
+ {
+ new PathFinding.Move(Direction.Up),
+ new PathFinding.Move(Direction.Left),
+ new PathFinding.Move(Direction.Right),
+ new PathFinding.Move(Direction.Down),
+ new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
+ new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
+ new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
+ new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
+ };
+
+ public static readonly List GoldGeneral = new(6)
+ {
+ new PathFinding.Move(Direction.Up),
+ new PathFinding.Move(Direction.UpLeft),
+ new PathFinding.Move(Direction.UpRight),
+ new PathFinding.Move(Direction.Left),
+ new PathFinding.Move(Direction.Right),
+ new PathFinding.Move(Direction.Down)
+ };
+
+ public static readonly List Knight = new(2)
+ {
+ new PathFinding.Move(Direction.KnightLeft),
+ new PathFinding.Move(Direction.KnightRight)
+ };
+
+ public static readonly List Lance = new(1)
+ {
+ new PathFinding.Move(Direction.Up, Distance.MultiStep),
+ };
+
+ public static readonly List Pawn = new(1)
+ {
+ new PathFinding.Move(Direction.Up)
+ };
+
+ public static readonly List Rook = new(4)
+ {
+ new PathFinding.Move(Direction.Up, Distance.MultiStep),
+ new PathFinding.Move(Direction.Left, Distance.MultiStep),
+ new PathFinding.Move(Direction.Right, Distance.MultiStep),
+ new PathFinding.Move(Direction.Down, Distance.MultiStep)
+ };
+
+ public static readonly List PromotedRook = new(8)
+ {
+ new PathFinding.Move(Direction.Up, Distance.MultiStep),
+ new PathFinding.Move(Direction.Left, Distance.MultiStep),
+ new PathFinding.Move(Direction.Right, Distance.MultiStep),
+ new PathFinding.Move(Direction.Down, Distance.MultiStep),
+ new PathFinding.Move(Direction.UpLeft),
+ new PathFinding.Move(Direction.UpRight),
+ new PathFinding.Move(Direction.DownLeft),
+ new PathFinding.Move(Direction.DownRight)
+ };
+
+ public static readonly List SilverGeneral = new(4)
+ {
+ new PathFinding.Move(Direction.Up),
+ new PathFinding.Move(Direction.UpLeft),
+ new PathFinding.Move(Direction.UpRight),
+ new PathFinding.Move(Direction.DownLeft),
+ new PathFinding.Move(Direction.DownRight)
+ };
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Models/Piece.cs b/Gameboard.ShogiUI.Sockets/Models/Piece.cs
new file mode 100644
index 0000000..7b86808
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/Piece.cs
@@ -0,0 +1,70 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using PathFinding;
+using System.Diagnostics;
+
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ [DebuggerDisplay("{WhichPiece} {Owner}")]
+ public class Piece : IPlanarElement
+ {
+ public WhichPiece WhichPiece { get; }
+ public WhichPerspective Owner { get; private set; }
+ public bool IsPromoted { get; private set; }
+ public bool IsUpsideDown => Owner == WhichPerspective.Player2;
+
+ public Piece(WhichPiece piece, WhichPerspective owner, bool isPromoted = false)
+ {
+ WhichPiece = piece;
+ Owner = owner;
+ IsPromoted = isPromoted;
+ }
+ public Piece(Piece piece) : this(piece.WhichPiece, piece.Owner, piece.IsPromoted)
+ {
+ }
+
+ public bool CanPromote => !IsPromoted
+ && WhichPiece != WhichPiece.King
+ && WhichPiece != WhichPiece.GoldGeneral;
+
+ public void ToggleOwnership()
+ {
+ Owner = Owner == WhichPerspective.Player1
+ ? WhichPerspective.Player2
+ : WhichPerspective.Player1;
+ }
+
+ public void Promote() => IsPromoted = CanPromote;
+
+ public void Demote() => IsPromoted = false;
+
+ public void Capture()
+ {
+ ToggleOwnership();
+ Demote();
+ }
+
+ // TODO: There is no reason to make "new" MoveSets every time this property is accessed.
+ public MoveSet MoveSet => WhichPiece switch
+ {
+ WhichPiece.King => new MoveSet(this, MoveSets.King),
+ WhichPiece.GoldGeneral => new MoveSet(this, MoveSets.GoldGeneral),
+ WhichPiece.SilverGeneral => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.SilverGeneral),
+ WhichPiece.Bishop => new MoveSet(this, IsPromoted ? MoveSets.PromotedBishop : MoveSets.Bishop),
+ WhichPiece.Rook => new MoveSet(this, IsPromoted ? MoveSets.PromotedRook : MoveSets.Rook),
+ WhichPiece.Knight => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Knight),
+ WhichPiece.Lance => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Lance),
+ WhichPiece.Pawn => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Pawn),
+ _ => throw new System.NotImplementedException()
+ };
+
+ public ServiceModels.Types.Piece ToServiceModel()
+ {
+ return new ServiceModels.Types.Piece
+ {
+ IsPromoted = IsPromoted,
+ Owner = Owner,
+ WhichPiece = WhichPiece
+ };
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Models/Session.cs b/Gameboard.ShogiUI.Sockets/Models/Session.cs
new file mode 100644
index 0000000..fd73b40
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/Session.cs
@@ -0,0 +1,38 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using Newtonsoft.Json;
+using System.Collections.Concurrent;
+using System.Net.WebSockets;
+
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ public class Session
+ {
+ // TODO: Separate subscriptions to the Session from the Session.
+ [JsonIgnore] public ConcurrentDictionary Subscriptions { get; }
+ public string Name { get; }
+ public User Player1 { get; }
+ public User? Player2 { get; private set; }
+ public bool IsPrivate { get; }
+
+ // TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied.
+ public Shogi Shogi { get; }
+
+ public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null)
+ {
+ Subscriptions = new ConcurrentDictionary();
+
+ Name = name;
+ Player1 = player1;
+ Player2 = player2;
+ IsPrivate = isPrivate;
+ Shogi = shogi;
+ }
+
+ public void SetPlayer2(User user)
+ {
+ Player2 = user;
+ }
+
+ public Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs
new file mode 100644
index 0000000..350b273
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs
@@ -0,0 +1,37 @@
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ ///
+ /// A representation of a Session without the board and game-rules.
+ ///
+ public class SessionMetadata
+ {
+ public string Name { get; }
+ public User Player1 { get; }
+ public User? Player2 { get; private set; }
+ public bool IsPrivate { get; }
+
+ public SessionMetadata(string name, bool isPrivate, User player1, User? player2 = null)
+ {
+ Name = name;
+ IsPrivate = isPrivate;
+ Player1 = player1;
+ Player2 = player2;
+ }
+ public SessionMetadata(Session sessionModel)
+ {
+ Name = sessionModel.Name;
+ IsPrivate = sessionModel.IsPrivate;
+ Player1 = sessionModel.Player1;
+ Player2 = sessionModel.Player2;
+ }
+
+ public void SetPlayer2(User user)
+ {
+ Player2 = user;
+ }
+
+ public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id;
+
+ public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Models/Shogi.cs b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs
new file mode 100644
index 0000000..ec944a2
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/Shogi.cs
@@ -0,0 +1,463 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using Gameboard.ShogiUI.Sockets.Utilities;
+using PathFinding;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ ///
+ /// Facilitates Shogi board state transitions, cognisant of Shogi rules.
+ /// The board is always from Player1's perspective.
+ /// [0,0] is the lower-left position, [8,8] is the higher-right position
+ ///
+ public class Shogi
+ {
+ private delegate void MoveSetCallback(Piece piece, Vector2 position);
+ private readonly PathFinder2D pathFinder;
+ private Shogi? validationBoard;
+ private Vector2 player1King;
+ private Vector2 player2King;
+ private List Hand => WhoseTurn == WhichPerspective.Player1 ? Player1Hand : Player2Hand;
+ public List Player1Hand { get; }
+ public List Player2Hand { get; }
+ public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method
+ public List MoveHistory { get; }
+ public WhichPerspective WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPerspective.Player1 : WhichPerspective.Player2;
+ public WhichPerspective? InCheck { get; private set; }
+ public bool IsCheckmate { get; private set; }
+
+ public string Error { get; private set; }
+
+ public Shogi()
+ {
+ Board = new CoordsToNotationCollection();
+ MoveHistory = new List(20);
+ Player1Hand = new List();
+ Player2Hand = new List();
+ pathFinder = new PathFinder2D(Board, 9, 9);
+ player1King = new Vector2(4, 0);
+ player2King = new Vector2(4, 8);
+ Error = string.Empty;
+
+ InitializeBoardState();
+ }
+
+ public Shogi(IList moves) : this()
+ {
+ for (var i = 0; i < moves.Count; i++)
+ {
+ if (!Move(moves[i]))
+ {
+ // Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc.
+ throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}");
+ }
+ }
+ }
+
+ private Shogi(Shogi toCopy)
+ {
+ Board = new CoordsToNotationCollection();
+ foreach (var kvp in toCopy.Board)
+ {
+ Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value);
+ }
+
+ pathFinder = new PathFinder2D(Board, 9, 9);
+ MoveHistory = new List(toCopy.MoveHistory);
+ Player1Hand = new List(toCopy.Player1Hand);
+ Player2Hand = new List(toCopy.Player2Hand);
+ player1King = toCopy.player1King;
+ player2King = toCopy.player2King;
+ Error = toCopy.Error;
+ }
+
+ public bool Move(Move move)
+ {
+ var otherPlayer = WhoseTurn == WhichPerspective.Player1 ? WhichPerspective.Player2 : WhichPerspective.Player1;
+ var moveSuccess = TryMove(move);
+
+ if (!moveSuccess)
+ {
+ return false;
+ }
+
+ // Evaluate check
+ if (EvaluateCheckAfterMove(move, otherPlayer))
+ {
+ InCheck = otherPlayer;
+ IsCheckmate = EvaluateCheckmate();
+ }
+ else
+ {
+ InCheck = null;
+ }
+ return true;
+ }
+ ///
+ /// Attempts a given move. Returns false if the move is illegal.
+ ///
+ private bool TryMove(Move move)
+ {
+ // Try making the move in a "throw away" board.
+ if (validationBoard == null)
+ {
+ validationBoard = new Shogi(this);
+ }
+
+ var isValid = move.PieceFromHand.HasValue
+ ? validationBoard.PlaceFromHand(move)
+ : validationBoard.PlaceFromBoard(move);
+ if (!isValid)
+ {
+ // Surface the error description.
+ Error = validationBoard.Error;
+ // Invalidate the "throw away" board.
+ validationBoard = null;
+ return false;
+ }
+ // If already in check, assert the move that resulted in check no longer results in check.
+ if (InCheck == WhoseTurn)
+ {
+ if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn))
+ {
+ // Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn;
+ return false;
+ }
+ }
+
+ // The move is valid and legal; update board state.
+ if (move.PieceFromHand.HasValue) PlaceFromHand(move);
+ else PlaceFromBoard(move);
+ return true;
+ }
+ /// True if the move was successful.
+ private bool PlaceFromHand(Move move)
+ {
+ var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand);
+ if (index < 0)
+ {
+ Error = $"{move.PieceFromHand} does not exist in the hand.";
+ return false;
+ }
+ if (Board[move.To] != null)
+ {
+ Error = $"Illegal move - attempting to capture while playing a piece from the hand.";
+ return false;
+ }
+
+ switch (move.PieceFromHand!.Value)
+ {
+ case WhichPiece.Knight:
+ {
+ // Knight cannot be placed onto the farthest two ranks from the hand.
+ if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y > 6)
+ || (WhoseTurn == WhichPerspective.Player2 && move.To.Y < 2))
+ {
+ Error = $"Knight has no valid moves after placed.";
+ return false;
+ }
+ break;
+ }
+ case WhichPiece.Lance:
+ case WhichPiece.Pawn:
+ {
+ // Lance and Pawn cannot be placed onto the farthest rank from the hand.
+ if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y == 8)
+ || (WhoseTurn == WhichPerspective.Player2 && move.To.Y == 0))
+ {
+ Error = $"{move.PieceFromHand} has no valid moves after placed.";
+ return false;
+ }
+ break;
+ }
+ }
+
+ // Mutate the board.
+ Board[move.To] = Hand[index];
+ Hand.RemoveAt(index);
+ MoveHistory.Add(move);
+
+ return true;
+ }
+ /// True if the move was successful.
+ private bool PlaceFromBoard(Move move)
+ {
+ var fromPiece = Board[move.From!.Value];
+ if (fromPiece == null)
+ {
+ Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}.";
+ return false; // Invalid move
+ }
+ if (fromPiece.Owner != WhoseTurn)
+ {
+ Error = "Not allowed to move the opponents piece";
+ return false; // Invalid move; cannot move other players pieces.
+ }
+ if (IsPathable(move.From.Value, move.To) == false)
+ {
+ Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set.";
+ return false; // Invalid move; move not part of move-set.
+ }
+
+ var captured = Board[move.To];
+ if (captured != null)
+ {
+ if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece.
+ captured.Capture();
+ Hand.Add(captured);
+ }
+
+ //Mutate the board.
+ if (move.IsPromotion)
+ {
+ if (WhoseTurn == WhichPerspective.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5))
+ {
+ fromPiece.Promote();
+ }
+ else if (WhoseTurn == WhichPerspective.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3))
+ {
+ fromPiece.Promote();
+ }
+ }
+ Board[move.To] = fromPiece;
+ Board[move.From!.Value] = null;
+ if (fromPiece.WhichPiece == WhichPiece.King)
+ {
+ if (fromPiece.Owner == WhichPerspective.Player1)
+ {
+ player1King.X = move.To.X;
+ player1King.Y = move.To.Y;
+ }
+ else if (fromPiece.Owner == WhichPerspective.Player2)
+ {
+ player2King.X = move.To.X;
+ player2King.Y = move.To.Y;
+ }
+ }
+ MoveHistory.Add(move);
+ return true;
+ }
+
+ private bool IsPathable(Vector2 from, Vector2 to)
+ {
+ var piece = Board[from];
+ if (piece == null) return false;
+
+ var isObstructed = false;
+ var isPathable = pathFinder.PathTo(from, to, (other, position) =>
+ {
+ if (other.Owner == piece.Owner) isObstructed = true;
+ });
+ return !isObstructed && isPathable;
+ }
+
+ #region Rules Validation
+ private bool EvaluateCheckAfterMove(Move move, WhichPerspective WhichPerspective)
+ {
+ if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother.
+
+ var isCheck = false;
+ var kingPosition = WhichPerspective == WhichPerspective.Player1 ? player1King : player2King;
+
+ // Check if the move put the king in check.
+ if (pathFinder.PathTo(move.To, kingPosition)) return true;
+
+ if (move.From.HasValue)
+ {
+ // Get line equation from king through the now-unoccupied location.
+ var direction = Vector2.Subtract(kingPosition, move.From!.Value);
+ var slope = Math.Abs(direction.Y / direction.X);
+ // If absolute slope is 45°, look for a bishop along the line.
+ // If absolute slope is 0° or 90°, look for a rook along the line.
+ // if absolute slope is 0°, look for lance along the line.
+ if (float.IsInfinity(slope))
+ {
+ // if slope of the move is also infinity...can skip this?
+ pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
+ {
+ if (piece.Owner != WhichPerspective)
+ {
+ switch (piece.WhichPiece)
+ {
+ case WhichPiece.Rook:
+ isCheck = true;
+ break;
+ case WhichPiece.Lance:
+ if (!piece.IsPromoted) isCheck = true;
+ break;
+ }
+ }
+ });
+ }
+ else if (slope == 1)
+ {
+ pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
+ {
+ if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop)
+ {
+ isCheck = true;
+ }
+ });
+ }
+ else if (slope == 0)
+ {
+ pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
+ {
+ if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook)
+ {
+ isCheck = true;
+ }
+ });
+ }
+ }
+ else
+ {
+ // TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent.
+ // Go read the shogi rules to be sure this is true.
+ }
+
+ return isCheck;
+ }
+ private bool EvaluateCheckmate()
+ {
+ if (!InCheck.HasValue) return false;
+
+ // Assume true and try to disprove.
+ var isCheckmate = true;
+ Board.ForEachNotNull((piece, from) => // For each piece...
+ {
+ // Short circuit
+ if (!isCheckmate) return;
+
+ if (piece.Owner == InCheck) // ...owned by the player in check...
+ {
+ // ...evaluate if any move gets the player out of check.
+ pathFinder.PathEvery(from, (other, position) =>
+ {
+ if (validationBoard == null) validationBoard = new Shogi(this);
+ var moveToTry = new Move(from, position);
+ var moveSuccess = validationBoard.TryMove(moveToTry);
+ if (moveSuccess)
+ {
+ validationBoard = null;
+ if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value))
+ {
+ isCheckmate = false;
+ }
+ }
+ });
+ }
+ });
+ return isCheckmate;
+ }
+ #endregion
+
+ private void InitializeBoardState()
+ {
+ Board["A1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1);
+ Board["B1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1);
+ Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1);
+ Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1);
+ Board["E1"] = new Piece(WhichPiece.King, WhichPerspective.Player1);
+ Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1);
+ Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1);
+ Board["H1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1);
+ Board["I1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1);
+
+ Board["A2"] = null;
+ Board["B2"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player1);
+ Board["C2"] = null;
+ Board["D2"] = null;
+ Board["E2"] = null;
+ Board["F2"] = null;
+ Board["G2"] = null;
+ Board["H2"] = new Piece(WhichPiece.Rook, WhichPerspective.Player1);
+ Board["I2"] = null;
+
+ Board["A3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+ Board["B3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+ Board["C3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+ Board["D3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+ Board["E3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+ Board["F3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+ Board["G3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+ Board["H3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+ Board["I3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
+
+ Board["A4"] = null;
+ Board["B4"] = null;
+ Board["C4"] = null;
+ Board["D4"] = null;
+ Board["E4"] = null;
+ Board["F4"] = null;
+ Board["G4"] = null;
+ Board["H4"] = null;
+ Board["I4"] = null;
+
+ Board["A5"] = null;
+ Board["B5"] = null;
+ Board["C5"] = null;
+ Board["D5"] = null;
+ Board["E5"] = null;
+ Board["F5"] = null;
+ Board["G5"] = null;
+ Board["H5"] = null;
+ Board["I5"] = null;
+
+ Board["A6"] = null;
+ Board["B6"] = null;
+ Board["C6"] = null;
+ Board["D6"] = null;
+ Board["E6"] = null;
+ Board["F6"] = null;
+ Board["G6"] = null;
+ Board["H6"] = null;
+ Board["I6"] = null;
+
+ Board["A7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+ Board["B7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+ Board["C7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+ Board["D7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+ Board["E7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+ Board["F7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+ Board["G7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+ Board["H7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+ Board["I7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
+
+ Board["A8"] = null;
+ Board["B8"] = new Piece(WhichPiece.Rook, WhichPerspective.Player2);
+ Board["C8"] = null;
+ Board["D8"] = null;
+ Board["E8"] = null;
+ Board["F8"] = null;
+ Board["G8"] = null;
+ Board["H8"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player2);
+ Board["I8"] = null;
+
+ Board["A9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2);
+ Board["B9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2);
+ Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2);
+ Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2);
+ Board["E9"] = new Piece(WhichPiece.King, WhichPerspective.Player2);
+ Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2);
+ Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2);
+ Board["H9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2);
+ Board["I9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2);
+ }
+
+ public BoardState ToServiceModel()
+ {
+ return new BoardState
+ {
+ Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()),
+ PlayerInCheck = InCheck,
+ WhoseTurn = WhoseTurn,
+ Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(),
+ Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList()
+ };
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs
new file mode 100644
index 0000000..e633524
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/User.cs
@@ -0,0 +1,87 @@
+using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Security.Claims;
+
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ public class User
+ {
+ public static readonly ReadOnlyCollection Adjectives = new(new[] {
+ "Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly"
+ });
+ public static readonly ReadOnlyCollection Subjects = new(new[] {
+ "Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato", "Penguin"
+ });
+ public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft);
+ public static User CreateGuestUser(string id)
+ {
+ var random = new Random();
+ // Adjective
+ var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count);
+ var adj = Adjectives[index];
+ // Subject
+ index = (int)Math.Floor(random.NextDouble() * Subjects.Count);
+ var subj = Subjects[index];
+
+ return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest);
+ }
+
+ public string Id { get; }
+ public string DisplayName { get; }
+
+ public WhichLoginPlatform LoginPlatform { get; }
+
+ public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest;
+
+ public bool IsAdmin => LoginPlatform == WhichLoginPlatform.Microsoft && Id == "Hauth@live.com";
+
+ public User(string id, string displayName, WhichLoginPlatform platform)
+ {
+ Id = id;
+ DisplayName = displayName;
+ LoginPlatform = platform;
+ }
+
+ public User(UserDocument document)
+ {
+ Id = document.Id;
+ DisplayName = document.DisplayName;
+ LoginPlatform = document.Platform;
+ }
+
+ public ClaimsIdentity CreateClaimsIdentity()
+ {
+ if (LoginPlatform == WhichLoginPlatform.Guest)
+ {
+ var claims = new List(4)
+ {
+ new Claim(ClaimTypes.NameIdentifier, Id),
+ new Claim(ClaimTypes.Name, DisplayName),
+ new Claim(ClaimTypes.Role, "Guest"),
+ new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
+ };
+ return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
+ }
+ else
+ {
+ var claims = new List(3)
+ {
+ new Claim(ClaimTypes.NameIdentifier, Id),
+ new Claim(ClaimTypes.Name, DisplayName),
+ new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
+ };
+ return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
+ }
+ }
+
+ public ServiceModels.Types.User ToServiceModel() => new()
+ {
+ Id = Id,
+ Name = DisplayName
+ };
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
new file mode 100644
index 0000000..5d2378e
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
@@ -0,0 +1,9 @@
+namespace Gameboard.ShogiUI.Sockets.Models
+{
+ public enum WhichLoginPlatform
+ {
+ Unknown,
+ Microsoft,
+ Guest
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json
index 20f6c84..05ff5bf 100644
--- a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json
+++ b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json
@@ -1,13 +1,13 @@
{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
- "applicationUrl": "http://localhost:63676",
- "sslPort": 44396
+ "applicationUrl": "http://localhost:50728/",
+ "sslPort": 44315
}
},
- "$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
@@ -16,13 +16,14 @@
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
- "AspShogiSockets": {
+ "Kestrel": {
"commandName": "Project",
- "launchUrl": "Socket/Token",
+ "launchBrowser": true,
+ "launchUrl": "/swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
- "applicationUrl": "http://127.0.0.1:5100"
+ "applicationUrl": "http://localhost:5100"
}
}
}
\ No newline at end of file
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs
new file mode 100644
index 0000000..ecd0450
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs
@@ -0,0 +1,65 @@
+using Gameboard.ShogiUI.Sockets.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ public class BoardStateDocument : CouchDocument
+ {
+ public string Name { get; set; }
+
+ ///
+ /// A dictionary where the key is a board-notation position, like D3.
+ ///
+ public Dictionary Board { get; set; }
+
+ public Piece[] Player1Hand { get; set; }
+
+ public Piece[] Player2Hand { get; set; }
+
+ ///
+ /// Move is null for first BoardState of a session - before anybody has made moves.
+ ///
+ public Move? Move { get; set; }
+
+ ///
+ /// Default constructor and setters are for deserialization.
+ ///
+ public BoardStateDocument() : base(WhichDocumentType.BoardState)
+ {
+ Name = string.Empty;
+ Board = new Dictionary(81, StringComparer.OrdinalIgnoreCase);
+ Player1Hand = Array.Empty();
+ Player2Hand = Array.Empty();
+ }
+
+ public BoardStateDocument(string sessionName, Models.Shogi shogi)
+ : base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState)
+ {
+ Name = sessionName;
+ Board = new Dictionary(81, StringComparer.OrdinalIgnoreCase);
+
+ for (var x = 0; x < 9; x++)
+ for (var y = 0; y < 9; y++)
+ {
+ var position = new Vector2(x, y);
+ var piece = shogi.Board[y, x];
+
+ if (piece != null)
+ {
+ var positionNotation = NotationHelper.ToBoardNotation(position);
+ Board[positionNotation] = new Piece(piece);
+ }
+ }
+
+ Player1Hand = shogi.Player1Hand.Select(model => new Piece(model)).ToArray();
+ Player2Hand = shogi.Player2Hand.Select(model => new Piece(model)).ToArray();
+ if (shogi.MoveHistory.Count > 0)
+ {
+ Move = new Move(shogi.MoveHistory[^1]);
+ }
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs
new file mode 100644
index 0000000..3435e7b
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs
@@ -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;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs
new file mode 100644
index 0000000..29d19c3
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs
@@ -0,0 +1,26 @@
+using Newtonsoft.Json;
+using System;
+
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ public abstract class CouchDocument
+ {
+ [JsonProperty("_id")] public string Id { get; set; }
+ [JsonProperty("_rev")] public string? RevisionId { get; set; }
+ public WhichDocumentType DocumentType { get; }
+ public DateTimeOffset CreatedDate { get; set; }
+
+ public CouchDocument(WhichDocumentType documentType)
+ : this(string.Empty, documentType, DateTimeOffset.UtcNow) { }
+
+ public CouchDocument(string id, WhichDocumentType documentType)
+ : this(id, documentType, DateTimeOffset.UtcNow) { }
+
+ public CouchDocument(string id, WhichDocumentType documentType, DateTimeOffset createdDate)
+ {
+ Id = id;
+ DocumentType = documentType;
+ CreatedDate = createdDate;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs
new file mode 100644
index 0000000..daab934
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ internal class CouchFindResult
+ {
+ public T[] docs;
+ public string warning;
+
+ public CouchFindResult()
+ {
+ docs = Array.Empty();
+ warning = "";
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs
new file mode 100644
index 0000000..ca98d2e
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ public class CouchViewResult where T : class
+ {
+ public int total_rows;
+ public int offset;
+ public CouchViewResultRow[] rows;
+
+ public CouchViewResult()
+ {
+ rows = Array.Empty>();
+ }
+ }
+
+ public class CouchViewResultRow
+ {
+ public string id;
+ public T doc;
+
+ public CouchViewResultRow()
+ {
+ id = string.Empty;
+ doc = default!;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs
new file mode 100644
index 0000000..8b29998
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs
@@ -0,0 +1,56 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using System.Numerics;
+
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ public class Move
+ {
+ ///
+ /// A board coordinate, like A3 or G6. When null, look for PieceFromHand to exist.
+ ///
+ public string? From { get; set; }
+
+ public bool IsPromotion { get; set; }
+
+ ///
+ /// The piece placed from the player's hand.
+ ///
+ public WhichPiece? PieceFromHand { get; set; }
+
+ ///
+ /// A board coordinate, like A3 or G6.
+ ///
+ public string To { get; set; }
+
+ ///
+ /// Default constructor and setters are for deserialization.
+ ///
+ public Move()
+ {
+ To = string.Empty;
+ }
+
+ public Move(Models.Move move)
+ {
+ if (move.From.HasValue)
+ {
+ From = ToBoardNotation(move.From.Value);
+ }
+ IsPromotion = move.IsPromotion;
+ To = ToBoardNotation(move.To);
+ PieceFromHand = move.PieceFromHand;
+ }
+
+ private static readonly char A = 'A';
+ private static string ToBoardNotation(Vector2 vector)
+ {
+ var file = (char)(vector.X + A);
+ var rank = vector.Y + 1;
+ return $"{file}{rank}";
+ }
+
+ public Models.Move ToDomainModel() => PieceFromHand.HasValue
+ ? new(PieceFromHand.Value, To)
+ : new(From!, To, IsPromotion);
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs
new file mode 100644
index 0000000..7f0be6f
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs
@@ -0,0 +1,27 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ public class Piece
+ {
+ public bool IsPromoted { get; set; }
+ public WhichPerspective Owner { get; set; }
+ public WhichPiece WhichPiece { get; set; }
+
+ ///
+ /// Default constructor and setters are for deserialization.
+ ///
+ public Piece()
+ {
+ }
+
+ public Piece(Models.Piece piece)
+ {
+ IsPromoted = piece.IsPromoted;
+ Owner = piece.Owner;
+ WhichPiece = piece.WhichPiece;
+ }
+
+ public Models.Piece ToDomainModel() => new(WhichPiece, Owner, IsPromoted);
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs
new file mode 100644
index 0000000..6db129e
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ public class SessionDocument : CouchDocument
+ {
+ public string Name { get; set; }
+ public string Player1Id { get; set; }
+ public string? Player2Id { get; set; }
+ public bool IsPrivate { get; set; }
+ public IList History { get; set; }
+
+ ///
+ /// Default constructor and setters are for deserialization.
+ ///
+ public SessionDocument() : base(WhichDocumentType.Session)
+ {
+ Name = string.Empty;
+ Player1Id = string.Empty;
+ Player2Id = string.Empty;
+ History = new List(0);
+ }
+
+ public SessionDocument(Models.Session session)
+ : base(session.Name, WhichDocumentType.Session)
+ {
+ Name = session.Name;
+ Player1Id = session.Player1.Id;
+ Player2Id = session.Player2?.Id;
+ IsPrivate = session.IsPrivate;
+ History = new List(0);
+ }
+
+ public SessionDocument(Models.SessionMetadata sessionMetaData)
+ : base(sessionMetaData.Name, WhichDocumentType.Session)
+ {
+ Name = sessionMetaData.Name;
+ Player1Id = sessionMetaData.Player1.Id;
+ Player2Id = sessionMetaData.Player2?.Id;
+ IsPrivate = sessionMetaData.IsPrivate;
+ History = new List(0);
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs
new file mode 100644
index 0000000..940a8bd
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs
@@ -0,0 +1,27 @@
+using Gameboard.ShogiUI.Sockets.Models;
+
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ public class UserDocument : CouchDocument
+ {
+ public string DisplayName { get; set; }
+ public WhichLoginPlatform Platform { get; set; }
+
+ ///
+ /// Constructor for JSON deserializing.
+ ///
+ public UserDocument() : base(WhichDocumentType.User)
+ {
+ DisplayName = string.Empty;
+ }
+
+ public UserDocument(
+ string id,
+ string displayName,
+ WhichLoginPlatform platform) : base(id, WhichDocumentType.User)
+ {
+ DisplayName = displayName;
+ Platform = platform;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs
new file mode 100644
index 0000000..f5a4f72
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs
@@ -0,0 +1,9 @@
+namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
+{
+ public enum WhichDocumentType
+ {
+ User,
+ Session,
+ BoardState
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs
index 07504fc..3489693 100644
--- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs
+++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs
@@ -1,128 +1,292 @@
-using Gameboard.Shogi.Api.ServiceModels.Messages;
-using Gameboard.ShogiUI.Sockets.Repositories.Utility;
+using Gameboard.ShogiUI.Sockets.Extensions;
+using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
+using System.Web;
namespace Gameboard.ShogiUI.Sockets.Repositories
{
public interface IGameboardRepository
{
- Task DeleteGame(string gameName);
- Task GetGame(string gameName);
- Task GetGames();
- Task GetGames(string playerName);
- Task GetMoves(string gameName);
- Task PostSession(PostSession request);
- Task PostJoinPrivateSession(PostJoinPrivateSession request);
- Task PutJoinPublicSession(string gameName, PutJoinPublicSession request);
- Task PostMove(string gameName, PostMove request);
- Task PostJoinCode(string gameName, string userName);
- Task GetPlayer(string userName);
- Task PostPlayer(PostPlayer request);
+ Task CreateBoardState(Models.Session session);
+ Task CreateSession(Models.SessionMetadata session);
+ Task CreateUser(Models.User user);
+ Task> ReadSessionMetadatas();
+ Task ReadSession(string name);
+ Task UpdateSession(Models.SessionMetadata session);
+ Task ReadSessionMetaData(string name);
+ Task ReadUser(string userName);
}
public class GameboardRepository : IGameboardRepository
{
- private readonly IAuthenticatedHttpClient client;
- public GameboardRepository(IAuthenticatedHttpClient client)
+ ///
+ /// Returns session, board state, and user documents, grouped by session.
+ ///
+ private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate";
+ ///
+ /// Returns session and user documents, grouped by session.
+ ///
+ private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata";
+ private static readonly string View_User = "_design/user/_view/user";
+ private const string ApplicationJson = "application/json";
+ private readonly HttpClient client;
+ private readonly ILogger logger;
+
+ public GameboardRepository(IHttpClientFactory clientFactory, ILogger logger)
{
- this.client = client;
+ client = clientFactory.CreateClient("couchdb");
+ this.logger = logger;
}
- public async Task GetGames()
+ public async Task> ReadSessionMetadatas()
{
- var response = await client.GetAsync("Sessions");
- var json = await response.Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
+ var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
+ var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(responseContent);
+ if (result != null)
+ {
+ var groupedBySession = result.rows.GroupBy(row => row.id);
+ var sessions = new List(result.total_rows / 3);
+ foreach (var group in groupedBySession)
+ {
+ /**
+ * A group contains 3 elements.
+ * 1) The session metadata.
+ * 2) User document of Player1.
+ * 3) User document of Player2.
+ */
+ var session = group.FirstOrDefault()?.doc.ToObject();
+ var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject();
+ var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject();
+ if (session != null && player1Doc != null)
+ {
+ var player2 = player2Doc == null ? null : new Models.User(player2Doc);
+ sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2));
+ }
+ }
+ return new Collection(sessions);
+ }
+ return new Collection(Array.Empty());
}
- public async Task GetGames(string playerName)
+ public async Task ReadSession(string name)
{
- var uri = $"Sessions/{playerName}";
- var response = await client.GetAsync(Uri.EscapeUriString(uri));
- var json = await response.Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
+ var queryParams = new QueryBuilder
+ {
+ { "include_docs", "true" },
+ { "startkey", JsonConvert.SerializeObject(new [] {name}) },
+ { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
+ }.ToQueryString();
+ var query = $"{View_SessionWithBoardState}{queryParams}";
+ logger.LogInformation("ReadSession() query: {query}", query);
+ var response = await client.GetAsync(query);
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(responseContent);
+ if (result != null && result.rows.Length > 2)
+ {
+ var group = result.rows;
+ /**
+ * A group contains 3 type of elements.
+ * 1) The session metadata.
+ * 2) User documents of Player1 and Player2.
+ * 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :(
+ * 3) BoardState
+ */
+ var session = group[0].doc.ToObject();
+ var player1Doc = group[1].doc.ToObject();
+ var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value();
+ var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
+ ? group[2].doc.ToObject()
+ : null;
+ var moves = group
+ .Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session.
+ // TODO: Deserialize just the Move property.
+ .Select(row => row.doc.ToObject())
+ .Select(boardState =>
+ {
+ var move = boardState!.Move!;
+ return move.PieceFromHand.HasValue
+ ? new Models.Move(move.PieceFromHand.Value, move.To)
+ : new Models.Move(move.From!, move.To, move.IsPromotion);
+ })
+ .ToList();
+
+ var shogi = new Models.Shogi(moves);
+ if (session != null && player1Doc != null)
+ {
+ var player2 = player2Doc == null ? null : new Models.User(player2Doc);
+ return new Models.Session(session.Name, session.IsPrivate, shogi, new(player1Doc), player2);
+ }
+ }
+ return null;
}
- public async Task GetGame(string gameName)
+ public async Task ReadSessionMetaData(string name)
{
- var uri = $"Session/{gameName}";
- var response = await client.GetAsync(Uri.EscapeUriString(uri));
- var json = await response.Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
+ var queryParams = new QueryBuilder
+ {
+ { "include_docs", "true" },
+ { "startkey", JsonConvert.SerializeObject(new [] {name}) },
+ { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
+ }.ToQueryString();
+ var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(responseContent);
+ if (result != null && result.rows.Length > 2)
+ {
+ var group = result.rows;
+ /**
+ * A group contains 3 elements.
+ * 1) The session metadata.
+ * 2) User document of Player1.
+ * 3) User document of Player2.
+ */
+ var session = group[0].doc.ToObject();
+ var player1Doc = group[1].doc.ToObject();
+ var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value();
+ var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
+ ? group[2].doc.ToObject()
+ : null;
+ if (session != null && player1Doc != null)
+ {
+ var player2 = player2Doc == null ? null : new Models.User(player2Doc);
+ return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2);
+ }
+ }
+ return null;
}
- public async Task DeleteGame(string gameName)
+ ///
+ /// Saves a snapshot of board state and the most recent move.
+ ///
+ public async Task CreateBoardState(Models.Session session)
{
- var uri = $"Session/{gameName}";
- await client.DeleteAsync(Uri.EscapeUriString(uri));
+ var boardStateDocument = new BoardStateDocument(session.Name, session.Shogi);
+ var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
+ var response = await client.PostAsync(string.Empty, content);
+ return response.IsSuccessStatusCode;
}
- public async Task PostSession(PostSession request)
+ public async Task CreateSession(Models.SessionMetadata session)
{
- var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
- var response = await client.PostAsync("Session", content);
- var json = await response.Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
+ var sessionDocument = new SessionDocument(session);
+ var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
+ var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent);
+
+ var boardStateDocument = new BoardStateDocument(session.Name, new Models.Shogi());
+ var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
+
+ if ((await postSessionDocumentTask).IsSuccessStatusCode)
+ {
+ var response = await client.PostAsync(string.Empty, boardStateContent);
+ return response.IsSuccessStatusCode;
+ }
+ return false;
}
- public async Task PutJoinPublicSession(string gameName, PutJoinPublicSession request)
+ public async Task UpdateSession(Models.SessionMetadata session)
{
- var uri = $"Session/Join";
- var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
- var response = await client.PutAsync(Uri.EscapeUriString(uri), content);
- var json = await response.Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
+ // GET existing session to get revisionId.
+ var readResponse = await client.GetAsync(session.Name);
+ if (!readResponse.IsSuccessStatusCode) return false;
+ var sessionDocument = JsonConvert.DeserializeObject(await readResponse.Content.ReadAsStringAsync());
+
+ // PUT the document with the revisionId.
+ var couchModel = new SessionDocument(session)
+ {
+ RevisionId = sessionDocument?.RevisionId
+ };
+ var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
+ var response = await client.PutAsync(couchModel.Id, content);
+ return response.IsSuccessStatusCode;
+ }
+ //public async Task PutJoinPublicSession(PutJoinPublicSession request)
+ //{
+ // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
+ // var response = await client.PutAsync(JoinSessionRoute, content);
+ // var json = await response.Content.ReadAsStringAsync();
+ // return JsonConvert.DeserializeObject(json).JoinSucceeded;
+ //}
+
+ //public async Task PostJoinPrivateSession(PostJoinPrivateSession request)
+ //{
+ // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
+ // var response = await client.PostAsync(JoinSessionRoute, content);
+ // var json = await response.Content.ReadAsStringAsync();
+ // var deserialized = JsonConvert.DeserializeObject(json);
+ // if (deserialized.JoinSucceeded)
+ // {
+ // return deserialized.SessionName;
+ // }
+ // return null;
+ //}
+
+ //public async Task> GetMoves(string gameName)
+ //{
+ // var uri = $"Session/{gameName}/Moves";
+ // var get = await client.GetAsync(Uri.EscapeUriString(uri));
+ // var json = await get.Content.ReadAsStringAsync();
+ // if (string.IsNullOrWhiteSpace(json))
+ // {
+ // return new List();
+ // }
+ // var response = JsonConvert.DeserializeObject(json);
+ // return response.Moves.Select(m => new Move(m)).ToList();
+ //}
+
+ //public async Task PostMove(string gameName, PostMove request)
+ //{
+ // var uri = $"Session/{gameName}/Move";
+ // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
+ // await client.PostAsync(Uri.EscapeUriString(uri), content);
+ //}
+
+ public async Task PostJoinCode(string gameName, string userName)
+ {
+ // var uri = $"JoinCode/{gameName}";
+ // var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName });
+ // var content = new StringContent(serialized, Encoding.UTF8, MediaType);
+ // var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync();
+ // return JsonConvert.DeserializeObject(json).JoinCode;
+ return string.Empty;
}
- public async Task PostJoinPrivateSession(PostJoinPrivateSession request)
+ public async Task ReadUser(string id)
{
- var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
- var response = await client.PostAsync("Session/Join", content);
- var json = await response.Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
+ var queryParams = new QueryBuilder
+ {
+ { "include_docs", "true" },
+ { "key", JsonConvert.SerializeObject(id) },
+ }.ToQueryString();
+ var response = await client.GetAsync($"{View_User}{queryParams}");
+ var responseContent = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(responseContent);
+ if (result != null && result.rows.Length > 0)
+ {
+ return new Models.User(result.rows[0].doc);
+ }
+
+ return null;
}
- public async Task GetMoves(string gameName)
+ public async Task CreateUser(Models.User user)
{
- var uri = $"Session/{gameName}/Moves";
- var response = await client.GetAsync(Uri.EscapeUriString(uri));
- var json = await response.Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
+ var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform);
+ var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
+ var response = await client.PostAsync(string.Empty, content);
+ return response.IsSuccessStatusCode;
}
- public async Task PostMove(string gameName, PostMove request)
- {
- var uri = $"Session/{gameName}/Move";
- var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
- await client.PostAsync(Uri.EscapeUriString(uri), content);
- }
-
- public async Task PostJoinCode(string gameName, string userName)
- {
- var uri = $"JoinCode/{gameName}";
- var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName });
- var content = new StringContent(serialized, Encoding.UTF8, "application/json");
- var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
- }
-
- public async Task GetPlayer(string playerName)
- {
- var uri = $"Player/{playerName}";
- var response = await client.GetAsync(Uri.EscapeUriString(uri));
- Console.WriteLine(response);
- var json = await response.Content.ReadAsStringAsync();
- return JsonConvert.DeserializeObject(json);
- }
-
- public async Task PostPlayer(PostPlayer request)
- {
- var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
- return await client.PostAsync("Player", content);
- }
}
}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs
deleted file mode 100644
index 55ff15a..0000000
--- a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using Gameboard.Shogi.Api.ServiceModels.Messages;
-using System;
-using System.Threading.Tasks;
-
-namespace Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers
-{
- public interface IGameboardRepositoryManager
- {
- Task CreateGuestUser();
- Task IsPlayer1(string sessionName, string playerName);
- bool IsGuest(string playerName);
- }
-
- public class GameboardRepositoryManager : IGameboardRepositoryManager
- {
- private const int MaxTries = 3;
- private const string GuestPrefix = "Guest-";
- private readonly IGameboardRepository repository;
-
- public GameboardRepositoryManager(IGameboardRepository repository)
- {
- this.repository = repository;
- }
-
- public async Task CreateGuestUser()
- {
- var count = 0;
- while (count < MaxTries)
- {
- count++;
- var clientId = $"Guest-{Guid.NewGuid()}";
- var request = new PostPlayer
- {
- PlayerName = clientId
- };
- var response = await repository.PostPlayer(request);
- if (response.IsSuccessStatusCode)
- {
- return clientId;
- }
- }
- throw new OperationCanceledException($"Failed to create guest user after {MaxTries} tries.");
- }
-
- public async Task IsPlayer1(string sessionName, string playerName)
- {
- var session = await repository.GetGame(sessionName);
- return session?.Session.Player1 == playerName;
- }
-
- public async Task CreateJoinCode(string sessionName, string playerName)
- {
- var getGameResponse = await repository.GetGame(sessionName);
- if (playerName == getGameResponse?.Session.Player1)
- {
- return (await repository.PostJoinCode(sessionName, playerName)).JoinCode;
- }
- return null;
- }
-
- public bool IsGuest(string playerName) => playerName.StartsWith(GuestPrefix);
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs
deleted file mode 100644
index 07a3df4..0000000
--- a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs
+++ /dev/null
@@ -1,120 +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 DeleteAsync(string requestUri);
- Task GetAsync(string requestUri);
- Task PostAsync(string requestUri, HttpContent content);
- Task PutAsync(string requestUri, HttpContent content);
- }
-
- public class AuthenticatedHttpClient : HttpClient, IAuthenticatedHttpClient
- {
- private readonly ILogger logger;
- private readonly string identityServerUrl;
- private TokenResponse tokenResponse;
- private readonly string clientId;
- private readonly string clientSecret;
-
- public AuthenticatedHttpClient(ILogger 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 async new Task PutAsync(string requestUri, HttpContent content)
- {
- var response = await base.PutAsync(requestUri, content);
- if (response.StatusCode == HttpStatusCode.Unauthorized)
- {
- await RefreshBearerToken();
- response = await base.PostAsync(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 async new Task 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 async new Task 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 async new Task 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;
- }
- }
-}
diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs
new file mode 100644
index 0000000..e1de3a2
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs
@@ -0,0 +1,15 @@
+using FluentValidation;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+
+namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
+{
+ public class JoinByCodeRequestValidator : AbstractValidator
+ {
+ public JoinByCodeRequestValidator()
+ {
+ RuleFor(_ => _.Action).Equal(ClientAction.JoinByCode);
+ RuleFor(_ => _.JoinCode).NotEmpty();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs
new file mode 100644
index 0000000..da598e3
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs
@@ -0,0 +1,15 @@
+using FluentValidation;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+
+namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators
+{
+ public class JoinGameRequestValidator : AbstractValidator
+ {
+ public JoinGameRequestValidator()
+ {
+ RuleFor(_ => _.Action).Equal(ClientAction.JoinGame);
+ RuleFor(_ => _.GameName).NotEmpty();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs
new file mode 100644
index 0000000..0418950
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Services/SocketService.cs
@@ -0,0 +1,132 @@
+using FluentValidation;
+using Gameboard.ShogiUI.Sockets.Extensions;
+using Gameboard.ShogiUI.Sockets.Managers;
+using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
+using Gameboard.ShogiUI.Sockets.Repositories;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+using Gameboard.ShogiUI.Sockets.Services.Utility;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+
+namespace Gameboard.ShogiUI.Sockets.Services
+{
+ public interface ISocketService
+ {
+ Task HandleSocketRequest(HttpContext context);
+ }
+
+ ///
+ /// Services a single websocket connection. Authenticates the socket connection, accepts messages, and sends messages.
+ ///
+ public class SocketService : ISocketService
+ {
+ private readonly ILogger logger;
+ private readonly ISocketConnectionManager communicationManager;
+ private readonly IGameboardRepository gameboardRepository;
+ private readonly IGameboardManager gameboardManager;
+ private readonly ISocketTokenCache tokenManager;
+ private readonly IJoinByCodeHandler joinByCodeHandler;
+ private readonly IValidator joinByCodeRequestValidator;
+ private readonly IValidator joinGameRequestValidator;
+
+ public SocketService(
+ ILogger logger,
+ ISocketConnectionManager communicationManager,
+ IGameboardRepository gameboardRepository,
+ IGameboardManager gameboardManager,
+ ISocketTokenCache tokenManager,
+ IJoinByCodeHandler joinByCodeHandler,
+ IValidator joinByCodeRequestValidator,
+ IValidator joinGameRequestValidator
+ ) : base()
+ {
+ this.logger = logger;
+ this.communicationManager = communicationManager;
+ this.gameboardRepository = gameboardRepository;
+ this.gameboardManager = gameboardManager;
+ this.tokenManager = tokenManager;
+ this.joinByCodeHandler = joinByCodeHandler;
+ this.joinByCodeRequestValidator = joinByCodeRequestValidator;
+ this.joinGameRequestValidator = joinGameRequestValidator;
+ }
+
+ public async Task HandleSocketRequest(HttpContext context)
+ {
+ if (!context.Request.Query.Keys.Contains("token"))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
+ return;
+ }
+ var token = Guid.Parse(context.Request.Query["token"][0]);
+ var userName = tokenManager.GetUsername(token);
+
+ if (string.IsNullOrEmpty(userName))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
+ return;
+ }
+ var socket = await context.WebSockets.AcceptWebSocketAsync();
+
+ communicationManager.SubscribeToBroadcast(socket, userName);
+ while (socket.State == WebSocketState.Open)
+ {
+ try
+ {
+ var message = await socket.ReceiveTextAsync();
+ if (string.IsNullOrWhiteSpace(message)) continue;
+ logger.LogInformation("Request \n{0}\n", message);
+ var request = JsonConvert.DeserializeObject(message);
+ if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action))
+ {
+ await socket.SendTextAsync("Error: Action not recognized.");
+ continue;
+ }
+ switch (request.Action)
+ {
+ case ClientAction.JoinByCode:
+ {
+ var req = JsonConvert.DeserializeObject(message);
+ if (req != null && await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req))
+ {
+ await joinByCodeHandler.Handle(req, userName);
+ }
+ break;
+ }
+ default:
+ await socket.SendTextAsync($"Received your message with action {request.Action}, but did no work.");
+ break;
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ logger.LogError(ex.Message);
+ }
+ catch (WebSocketException ex)
+ {
+ logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}.");
+ logger.LogInformation("Probably tried writing to a closed socket.");
+ logger.LogError(ex.Message);
+ }
+ communicationManager.UnsubscribeFromBroadcastAndGames(userName);
+ }
+ }
+
+ public async Task ValidateRequestAndReplyIfInvalid(WebSocket socket, IValidator validator, TRequest request)
+ {
+ var results = validator.Validate(request);
+ if (!results.IsValid)
+ {
+ var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage));
+ await socket.SendTextAsync(errors);
+ }
+ return results.IsValid;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs
similarity index 66%
rename from Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs
rename to Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs
index 9ac96f6..c4fde65 100644
--- a/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs
+++ b/Gameboard.ShogiUI.Sockets/Services/Utility/JsonRequest.cs
@@ -1,6 +1,6 @@
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
-namespace Gameboard.ShogiUI.Sockets.Managers.Utility
+namespace Gameboard.ShogiUI.Sockets.Services.Utility
{
public class JsonRequest
{
diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs
new file mode 100644
index 0000000..1928408
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs
@@ -0,0 +1,10 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
+
+namespace Gameboard.ShogiUI.Sockets.Services.Utility
+{
+ public class Request : IRequest
+ {
+ public ClientAction Action { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs
new file mode 100644
index 0000000..067d374
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs
@@ -0,0 +1,9 @@
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+
+namespace Gameboard.ShogiUI.Sockets.Services.Utility
+{
+ public class Response : IResponse
+ {
+ public string Action { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
new file mode 100644
index 0000000..fc00288
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
@@ -0,0 +1,43 @@
+using Gameboard.ShogiUI.Sockets.Repositories;
+using Microsoft.AspNetCore.Authentication;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace Gameboard.ShogiUI.Sockets
+{
+ ///
+ /// Standardizes the claims from third party issuers. Also registers new msal users in the database.
+ ///
+ public class ShogiUserClaimsTransformer : IClaimsTransformation
+ {
+ private static readonly string MsalUsernameClaim = "preferred_username";
+ private readonly IGameboardRepository gameboardRepository;
+
+ public ShogiUserClaimsTransformer(IGameboardRepository gameboardRepository)
+ {
+ this.gameboardRepository = gameboardRepository;
+ }
+
+ public async Task TransformAsync(ClaimsPrincipal principal)
+ {
+ var nameClaim = principal.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim);
+ if (nameClaim != default)
+ {
+ var user = await gameboardRepository.ReadUser(nameClaim.Value);
+ if (user == null)
+ {
+ var newUser = Models.User.CreateMsalUser(nameClaim.Value);
+ var success = await gameboardRepository.CreateUser(newUser);
+ if (success) user = newUser;
+ }
+
+ if (user != null)
+ {
+ return new ClaimsPrincipal(user.CreateClaimsIdentity());
+ }
+ }
+ return principal;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs
index 7d32044..c735d0e 100644
--- a/Gameboard.ShogiUI.Sockets/Startup.cs
+++ b/Gameboard.ShogiUI.Sockets/Startup.cs
@@ -1,149 +1,217 @@
-using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
+using FluentValidation;
using Gameboard.ShogiUI.Sockets.Extensions;
+using Gameboard.ShogiUI.Sockets.Managers;
+using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
+using Gameboard.ShogiUI.Sockets.Repositories;
+using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
+using Gameboard.ShogiUI.Sockets.Services;
+using Gameboard.ShogiUI.Sockets.Services.RequestValidators;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Http;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Web;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
-using Gameboard.ShogiUI.Sockets.Managers;
-using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
-using Gameboard.ShogiUI.Sockets.Repositories;
-using Gameboard.ShogiUI.Sockets.Repositories.Utility;
-using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets
{
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
- public IConfiguration Configuration { get; }
+ public IConfiguration Configuration { get; }
- // This method gets called by the runtime. Use this method to add services to the container.
- public void ConfigureServices(IServiceCollection services)
- {
- // Socket ActionHandlers
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton