The big merge
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -52,3 +52,4 @@ Thumbs.db
|
|||||||
#Luke
|
#Luke
|
||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
|
*.user
|
||||||
|
|||||||
13
Benchmarking/Benchmarking.csproj
Normal file
13
Benchmarking/Benchmarking.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
106
Benchmarking/Benchmarks.cs
Normal file
106
Benchmarking/Benchmarks.cs
Normal file
@@ -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<Benchmarks>();
|
||||||
|
Console.WriteLine("Done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs
Normal file
18
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs
Normal file
16
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSession.cs
Normal file
@@ -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; }
|
||||||
|
/// <summary>
|
||||||
|
/// The perspective on the game of the requesting user.
|
||||||
|
/// </summary>
|
||||||
|
public WhichPerspective PlayerPerspective { get; set; }
|
||||||
|
public BoardState BoardState { get; set; }
|
||||||
|
public IList<Move> MoveHistory { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs
Normal file
11
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs
Normal file
@@ -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<Game> PlayerHasJoinedSessions { get; set; }
|
||||||
|
public Collection<Game> AllOtherSessions { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs
Normal file
14
Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||||
{
|
{
|
||||||
public class PostGameInvitation
|
public class PostGameInvitation
|
||||||
{
|
{
|
||||||
11
Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
Normal file
11
Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api
|
||||||
|
{
|
||||||
|
public class PostSession
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool IsPrivate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||||
|
<AnalysisLevel>5</AnalysisLevel>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
21
Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs
Normal file
21
Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs
Normal file
@@ -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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The player who created the game.
|
||||||
|
/// </summary>
|
||||||
|
public string PlayerName { get; set; }
|
||||||
|
|
||||||
|
public CreateGameResponse()
|
||||||
|
{
|
||||||
|
Action = ClientAction.CreateGame.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||||
|
{
|
||||||
|
public interface IRequest
|
||||||
|
{
|
||||||
|
ClientAction Action { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket
|
||||||
|
{
|
||||||
|
public interface IResponse
|
||||||
|
{
|
||||||
|
string Action { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces
|
|
||||||
{
|
|
||||||
public interface IResponse
|
|
||||||
{
|
|
||||||
string Action { get; }
|
|
||||||
string Error { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs
Normal file
41
Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs
Normal file
@@ -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; }
|
||||||
|
/// <summary>
|
||||||
|
/// The player who joined the game.
|
||||||
|
/// </summary>
|
||||||
|
public string PlayerName { get; set; }
|
||||||
|
|
||||||
|
public JoinGameResponse()
|
||||||
|
{
|
||||||
|
Action = ClientAction.JoinGame.ToString();
|
||||||
|
GameName = "";
|
||||||
|
PlayerName = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JoinByCodeResponse : JoinGameResponse, IResponse
|
||||||
|
{
|
||||||
|
public JoinByCodeResponse()
|
||||||
|
{
|
||||||
|
Action = ClientAction.JoinByCode.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Game> Games { get; set; }
|
|
||||||
|
|
||||||
public ListGamesResponse(ClientAction action)
|
|
||||||
{
|
|
||||||
Action = action.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Move> Moves { get; set; }
|
|
||||||
public string Error { get; set; }
|
|
||||||
|
|
||||||
public LoadGameResponse(ClientAction action)
|
|
||||||
{
|
|
||||||
Action = action.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs
Normal file
19
Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs
Normal file
@@ -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; }
|
||||||
|
/// <summary>
|
||||||
|
/// The player that made the move.
|
||||||
|
/// </summary>
|
||||||
|
public string PlayerName { get; set; }
|
||||||
|
|
||||||
|
public MoveResponse()
|
||||||
|
{
|
||||||
|
Action = ClientAction.Move.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
|
||||||
{
|
|
||||||
public enum ClientAction
|
|
||||||
{
|
|
||||||
ListGames,
|
|
||||||
CreateGame,
|
|
||||||
JoinGame,
|
|
||||||
JoinByCode,
|
|
||||||
LoadGame,
|
|
||||||
Move,
|
|
||||||
KeepAlive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
|
||||||
{
|
|
||||||
public class Coords
|
|
||||||
{
|
|
||||||
public int X { get; set; }
|
|
||||||
public int Y { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types
|
|
||||||
{
|
|
||||||
public class Game
|
|
||||||
{
|
|
||||||
public string GameName { get; set; }
|
|
||||||
public string[] Players { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Toggles perspective of this move. (ie from player 1 to player 2)
|
|
||||||
/// </summary>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs
Normal file
14
Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||||
|
{
|
||||||
|
public class BoardState
|
||||||
|
{
|
||||||
|
public Dictionary<string, Piece?> Board { get; set; } = new Dictionary<string, Piece?>();
|
||||||
|
public IReadOnlyCollection<Piece> Player1Hand { get; set; } = Array.Empty<Piece>();
|
||||||
|
public IReadOnlyCollection<Piece> Player2Hand { get; set; } = Array.Empty<Piece>();
|
||||||
|
public WhichPerspective? PlayerInCheck { get; set; }
|
||||||
|
public WhichPerspective WhoseTurn { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||||
|
{
|
||||||
|
public enum ClientAction
|
||||||
|
{
|
||||||
|
CreateGame,
|
||||||
|
JoinGame,
|
||||||
|
JoinByCode,
|
||||||
|
Move
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs
Normal file
38
Gameboard.ShogiUI.Sockets.ServiceModels/Types/Game.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Players[0] is the session owner, Players[1] is the other person.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> Players
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var list = new List<string>(2) { Player1 };
|
||||||
|
if (!string.IsNullOrEmpty(Player2)) list.Add(Player2);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for serialization.
|
||||||
|
/// </summary>
|
||||||
|
public Game()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Game(string gameName, string player1, string? player2 = null)
|
||||||
|
{
|
||||||
|
GameName = gameName;
|
||||||
|
Player1 = player1;
|
||||||
|
Player2 = player2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs
Normal file
12
Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||||
|
{
|
||||||
|
public class Move
|
||||||
|
{
|
||||||
|
public WhichPiece? PieceFromCaptured { get; set; }
|
||||||
|
/// <summary>Board position notation, like A3 or G1</summary>
|
||||||
|
public string? From { get; set; }
|
||||||
|
/// <summary>Board position notation, like A3 or G1</summary>
|
||||||
|
public string To { get; set; } = string.Empty;
|
||||||
|
public bool IsPromotion { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs
Normal file
9
Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs
Normal file
9
Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||||
|
{
|
||||||
|
public enum WhichPerspective
|
||||||
|
{
|
||||||
|
Player1,
|
||||||
|
Player2,
|
||||||
|
Spectator
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs
Normal file
14
Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types
|
||||||
|
{
|
||||||
|
public enum WhichPiece
|
||||||
|
{
|
||||||
|
King,
|
||||||
|
GoldGeneral,
|
||||||
|
SilverGeneral,
|
||||||
|
Bishop,
|
||||||
|
Rook,
|
||||||
|
Knight,
|
||||||
|
Lance,
|
||||||
|
Pawn
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,26 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 16.0.30503.244
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets", "Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj", "{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}"
|
||||||
EndProject
|
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}"
|
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
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU
|
{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
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
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}
|
SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
|||||||
39
Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs
Normal file
39
Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets
|
||||||
|
{
|
||||||
|
namespace anonymous_session.Middlewares
|
||||||
|
{
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TODO: Use this example in the guest session logic instead of custom claims.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +1,269 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
|
using Gameboard.ShogiUI.Sockets.Managers;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages;
|
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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
|
||||||
[ApiController]
|
|
||||||
[Route("[controller]")]
|
|
||||||
public class GameController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IGameboardRepositoryManager manager;
|
|
||||||
private readonly IGameboardRepository repository;
|
|
||||||
|
|
||||||
public GameController(
|
[ApiController]
|
||||||
IGameboardRepository repository,
|
[Route("[controller]")]
|
||||||
IGameboardRepositoryManager manager)
|
[Authorize(Roles = "Shogi")]
|
||||||
{
|
public class GameController : ControllerBase
|
||||||
this.manager = manager;
|
{
|
||||||
this.repository = repository;
|
private readonly IGameboardManager gameboardManager;
|
||||||
}
|
private readonly IGameboardRepository gameboardRepository;
|
||||||
|
private readonly ISocketConnectionManager communicationManager;
|
||||||
|
|
||||||
[Route("JoinCode")]
|
public GameController(
|
||||||
public async Task<IActionResult> PostGameInvitation([FromBody] PostGameInvitation request)
|
IGameboardRepository repository,
|
||||||
{
|
IGameboardManager manager,
|
||||||
var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
|
ISocketConnectionManager communicationManager)
|
||||||
var isPlayer1 = await manager.IsPlayer1(request.SessionName, userName);
|
{
|
||||||
if (isPlayer1)
|
gameboardManager = manager;
|
||||||
{
|
gameboardRepository = repository;
|
||||||
var code = (await repository.PostJoinCode(request.SessionName, userName)).JoinCode;
|
this.communicationManager = communicationManager;
|
||||||
return new CreatedResult("", new PostGameInvitationResponse(code));
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return new UnauthorizedResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
[HttpPost("JoinCode")]
|
||||||
[Route("GuestJoinCode")]
|
public async Task<IActionResult> PostGameInvitation([FromBody] PostGameInvitation request)
|
||||||
public async Task<IActionResult> PostGuestGameInvitation([FromBody] PostGuestGameInvitation request)
|
{
|
||||||
{
|
|
||||||
|
|
||||||
var isGuest = manager.IsGuest(request.GuestId);
|
//var isPlayer1 = await gameboardManager.IsPlayer1(request.SessionName, userName);
|
||||||
var isPlayer1 = manager.IsPlayer1(request.SessionName, request.GuestId);
|
//if (isPlayer1)
|
||||||
if (isGuest && await isPlayer1)
|
//{
|
||||||
{
|
// var code = await gameboardRepository.PostJoinCode(request.SessionName, userName);
|
||||||
var code = (await repository.PostJoinCode(request.SessionName, request.GuestId)).JoinCode;
|
// return new CreatedResult("", new PostGameInvitationResponse(code));
|
||||||
return new CreatedResult("", new PostGameInvitationResponse(code));
|
//}
|
||||||
}
|
//else
|
||||||
else
|
//{
|
||||||
{
|
return new UnauthorizedResult();
|
||||||
return new UnauthorizedResult();
|
//}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
[AllowAnonymous]
|
||||||
|
[HttpPost("GuestJoinCode")]
|
||||||
|
public async Task<IActionResult> PostGuestGameInvitation([FromBody] PostGuestGameInvitation request)
|
||||||
|
{
|
||||||
|
|
||||||
|
//var isGuest = gameboardManager.IsGuest(request.GuestId);
|
||||||
|
//var isPlayer1 = gameboardManager.IsPlayer1(request.SessionName, request.GuestId);
|
||||||
|
//if (isGuest && await isPlayer1)
|
||||||
|
//{
|
||||||
|
// var code = await gameboardRepository.PostJoinCode(request.SessionName, request.GuestId);
|
||||||
|
// return new CreatedResult("", new PostGameInvitationResponse(code));
|
||||||
|
//}
|
||||||
|
//else
|
||||||
|
//{
|
||||||
|
return new UnauthorizedResult();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{gameName}/Move")]
|
||||||
|
public async Task<IActionResult> PostMove([FromRoute] string gameName, [FromBody] PostMove request)
|
||||||
|
{
|
||||||
|
var user = await gameboardManager.ReadUser(User);
|
||||||
|
var session = await gameboardRepository.ReadSession(gameName);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
if (user == null || (session.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<IActionResult> PostSession([FromBody] PostSession request)
|
||||||
|
//{
|
||||||
|
// var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2);
|
||||||
|
// var success = await repository.CreateSession(model);
|
||||||
|
// if (success)
|
||||||
|
// {
|
||||||
|
// var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.ClientAction.CreateGame)
|
||||||
|
// {
|
||||||
|
// Game = model.ToServiceModel(),
|
||||||
|
// PlayerName =
|
||||||
|
// }
|
||||||
|
// var task = request.IsPrivate
|
||||||
|
// ? communicationManager.BroadcastToPlayers(response, userName)
|
||||||
|
// : communicationManager.BroadcastToAll(response);
|
||||||
|
// return new CreatedResult("", null);
|
||||||
|
// }
|
||||||
|
// return new ConflictResult();
|
||||||
|
//}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> PostSession([FromBody] PostSession request)
|
||||||
|
{
|
||||||
|
var user = await ReadUserOrThrow();
|
||||||
|
var session = new 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the board session and subscribes the caller to socket events for that session.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{gameName}")]
|
||||||
|
public async Task<IActionResult> GetSession([FromRoute] string gameName)
|
||||||
|
{
|
||||||
|
var user = await ReadUserOrThrow();
|
||||||
|
var session = await gameboardRepository.ReadSession(gameName);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerPerspective = 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<GetSessionsResponse> 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<Game>(sessionsJoinedByUser),
|
||||||
|
AllOtherSessions = new Collection<Game>(sessionsNotJoinedByUser)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{gameName}")]
|
||||||
|
public async Task<IActionResult> PutJoinSession([FromRoute] string gameName)
|
||||||
|
{
|
||||||
|
var user = await ReadUserOrThrow();
|
||||||
|
var session = await gameboardRepository.ReadSessionMetaData(gameName);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
if (session.Player2 != null)
|
||||||
|
{
|
||||||
|
return this.Conflict("This session already has two seated players and is full.");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.SetPlayer2(user);
|
||||||
|
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<IActionResult> DeleteSession([FromRoute] string gameName)
|
||||||
|
{
|
||||||
|
var user = await ReadUserOrThrow();
|
||||||
|
if (user.IsAdmin)
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Models.User> ReadUserOrThrow()
|
||||||
|
{
|
||||||
|
var user = await gameboardManager.ReadUser(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("Unknown user claims.");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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;
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Api.Messages;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Linq;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Controllers
|
namespace Gameboard.ShogiUI.Sockets.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
[ApiController]
|
[Authorize(Roles = "Shogi")]
|
||||||
public class SocketController : ControllerBase
|
public class SocketController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ISocketTokenManager tokenManager;
|
private readonly ILogger<SocketController> logger;
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
private readonly ISocketTokenCache tokenCache;
|
||||||
private readonly IGameboardRepositoryManager gameboardManager;
|
private readonly IGameboardManager gameboardManager;
|
||||||
|
private readonly IGameboardRepository gameboardRepository;
|
||||||
|
private readonly ISocketConnectionManager connectionManager;
|
||||||
|
private readonly AuthenticationProperties authenticationProps;
|
||||||
|
|
||||||
public SocketController(
|
public SocketController(
|
||||||
ISocketTokenManager tokenManager,
|
ILogger<SocketController> logger,
|
||||||
IGameboardRepository gameboardRepository,
|
ISocketTokenCache tokenCache,
|
||||||
IGameboardRepositoryManager gameboardManager)
|
IGameboardManager gameboardManager,
|
||||||
{
|
IGameboardRepository gameboardRepository,
|
||||||
this.tokenManager = tokenManager;
|
ISocketConnectionManager connectionManager)
|
||||||
this.gameboardRepository = gameboardRepository;
|
{
|
||||||
this.gameboardManager = gameboardManager;
|
this.logger = logger;
|
||||||
}
|
this.tokenCache = tokenCache;
|
||||||
|
this.gameboardManager = gameboardManager;
|
||||||
|
this.gameboardRepository = gameboardRepository;
|
||||||
|
this.connectionManager = connectionManager;
|
||||||
|
|
||||||
[Route("Token")]
|
authenticationProps = new AuthenticationProperties
|
||||||
public IActionResult GetToken()
|
{
|
||||||
{
|
AllowRefresh = true,
|
||||||
var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
|
IsPersistent = true
|
||||||
var token = tokenManager.GenerateToken(userName);
|
};
|
||||||
return new JsonResult(new GetTokenResponse(token));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
[HttpGet("GuestLogout")]
|
||||||
[Route("GuestToken")]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> GetGuestToken([FromQuery] GetGuestToken request)
|
public async Task<IActionResult> GuestLogout()
|
||||||
{
|
{
|
||||||
if (request.ClientId == null)
|
var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
{
|
|
||||||
var clientId = await gameboardManager.CreateGuestUser();
|
var userId = User?.UserId();
|
||||||
var token = tokenManager.GenerateToken(clientId);
|
if (!string.IsNullOrEmpty(userId))
|
||||||
return new JsonResult(new GetGuestTokenResponse(clientId, token));
|
{
|
||||||
}
|
connectionManager.UnsubscribeFromBroadcastAndGames(userId);
|
||||||
else
|
}
|
||||||
{
|
|
||||||
var response = await gameboardRepository.GetPlayer(request.ClientId);
|
await signoutTask;
|
||||||
if (response != null && response.Player != null)
|
return Ok();
|
||||||
{
|
}
|
||||||
var token = tokenManager.GenerateToken(response.Player.Name);
|
|
||||||
return new JsonResult(new GetGuestTokenResponse(response.Player.Name, token));
|
[HttpGet("Token")]
|
||||||
}
|
public async Task<IActionResult> GetToken()
|
||||||
}
|
{
|
||||||
return new UnauthorizedResult();
|
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<IActionResult> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
Normal file
28
Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs
Normal file
@@ -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..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,50 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Extensions
|
namespace Gameboard.ShogiUI.Sockets.Extensions
|
||||||
{
|
{
|
||||||
public class LogMiddleware
|
public class LogMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate next;
|
private readonly RequestDelegate next;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
|
|
||||||
public LogMiddleware(RequestDelegate next, ILoggerFactory factory)
|
|
||||||
{
|
|
||||||
this.next = next;
|
|
||||||
logger = factory.CreateLogger<LogMiddleware>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
public LogMiddleware(RequestDelegate next, ILoggerFactory factory)
|
||||||
{
|
{
|
||||||
try
|
this.next = next;
|
||||||
{
|
logger = factory.CreateLogger<LogMiddleware>();
|
||||||
await next(context);
|
}
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
logger.LogInformation("Request {method} {url} => {statusCode}",
|
|
||||||
context.Request?.Method,
|
|
||||||
context.Request?.Path.Value,
|
|
||||||
context.Response?.StatusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class IApplicationBuilderExtensions
|
public async Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder)
|
try
|
||||||
{
|
{
|
||||||
builder.UseMiddleware<LogMiddleware>();
|
await next(context);
|
||||||
return builder;
|
}
|
||||||
}
|
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<LogMiddleware>();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs
Normal file
63
Gameboard.ShogiUI.Sockets/Extensions/ModelExtensions.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,27 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||||
|
<AnalysisLevel>5</AnalysisLevel>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Gameboard.Shogi.Api.ServiceModels" Version="2.10.0" />
|
<PackageReference Include="FluentValidation" Version="10.3.6" />
|
||||||
<PackageReference Include="IdentityModel" Version="5.0.0" />
|
<PackageReference Include="IdentityModel" Version="6.0.0-preview.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="5.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="1.5.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.5.1" />
|
<PackageReference Include="Microsoft.Identity.Web" Version="1.21.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.21.1" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
|
<PackageReference Include="NSwag.AspNetCore" Version="13.15.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
|
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
|
||||||
|
<ProjectReference Include="..\PathFinding\PathFinding.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
|
||||||
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
|
||||||
<Controller_SelectedScaffolderCategoryPath>root/Controller</Controller_SelectedScaffolderCategoryPath>
|
|
||||||
<ActiveDebugProfile>AspShogiSockets</ActiveDebugProfile>
|
|
||||||
<ShowAllFiles>false</ShowAllFiles>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
|
||||||
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -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<CreateGameHandler> logger;
|
|
||||||
private readonly IGameboardRepository repository;
|
|
||||||
private readonly ISocketCommunicationManager communicationManager;
|
|
||||||
|
|
||||||
public CreateGameHandler(
|
|
||||||
ILogger<CreateGameHandler> 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<CreateGameRequest>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Responsible for parsing json and handling the request.
|
|
||||||
/// </summary>
|
|
||||||
Task Handle(WebSocket socket, string json, string userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate IActionHandler ActionHandlerResolver(ClientAction action);
|
|
||||||
}
|
|
||||||
@@ -1,75 +1,64 @@
|
|||||||
using Gameboard.Shogi.Api.ServiceModels.Messages;
|
using Gameboard.ShogiUI.Sockets.Repositories;
|
||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
|
||||||
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;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers
|
||||||
{
|
{
|
||||||
public class JoinByCodeHandler : IActionHandler
|
public interface IJoinByCodeHandler
|
||||||
|
{
|
||||||
|
Task Handle(JoinByCodeRequest request, string userName);
|
||||||
|
}
|
||||||
|
public class JoinByCodeHandler : IJoinByCodeHandler
|
||||||
{
|
{
|
||||||
private readonly ILogger<JoinByCodeHandler> logger;
|
|
||||||
private readonly IGameboardRepository repository;
|
private readonly IGameboardRepository repository;
|
||||||
private readonly ISocketCommunicationManager communicationManager;
|
private readonly ISocketConnectionManager communicationManager;
|
||||||
|
|
||||||
public JoinByCodeHandler(
|
public JoinByCodeHandler(
|
||||||
ILogger<JoinByCodeHandler> logger,
|
ISocketConnectionManager communicationManager,
|
||||||
ISocketCommunicationManager communicationManager,
|
|
||||||
IGameboardRepository repository)
|
IGameboardRepository repository)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.communicationManager = communicationManager;
|
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<JoinByCode>(json);
|
||||||
var request = JsonConvert.DeserializeObject<JoinByCode>(json);
|
//var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession
|
||||||
var joinGameResponse = await repository.PostJoinPrivateSession(new PostJoinPrivateSession
|
//{
|
||||||
{
|
// PlayerName = userName,
|
||||||
PlayerName = userName,
|
// JoinCode = request.JoinCode
|
||||||
JoinCode = request.JoinCode
|
//});
|
||||||
});
|
|
||||||
|
|
||||||
if (joinGameResponse.JoinSucceeded)
|
//if (sessionName == null)
|
||||||
{
|
//{
|
||||||
var gameName = (await repository.GetGame(joinGameResponse.SessionName)).Session.Name;
|
// 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.
|
// // The player joining sees the JoinByCode occur.
|
||||||
var response = new JoinGameResponse(ClientAction.JoinGame)
|
// response = new JoinGameResponse(ClientAction.JoinByCode)
|
||||||
{
|
// {
|
||||||
PlayerName = userName,
|
// PlayerName = userName,
|
||||||
GameName = gameName
|
// GameName = sessionName
|
||||||
};
|
// };
|
||||||
var serialized = JsonConvert.SerializeObject(response);
|
// await communicationManager.BroadcastToPlayers(response, userName);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<JoinGameHandler> logger;
|
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
|
||||||
private readonly ISocketCommunicationManager communicationManager;
|
|
||||||
public JoinGameHandler(
|
|
||||||
ILogger<JoinGameHandler> 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<JoinGameRequest>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ListGamesHandler> logger;
|
|
||||||
private readonly IGameboardRepository repository;
|
|
||||||
|
|
||||||
public ListGamesHandler(
|
|
||||||
ILogger<ListGamesHandler> 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<ListGamesRequest>(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<Game>()
|
|
||||||
};
|
|
||||||
|
|
||||||
var serialized = JsonConvert.SerializeObject(response);
|
|
||||||
logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
|
|
||||||
await socket.SendTextAsync(serialized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<LoadGameHandler> logger;
|
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
|
||||||
private readonly ISocketCommunicationManager communicationManager;
|
|
||||||
|
|
||||||
public LoadGameHandler(
|
|
||||||
ILogger<LoadGameHandler> 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<LoadGameRequest>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<MoveHandler> logger;
|
|
||||||
private readonly IGameboardRepository gameboardRepository;
|
|
||||||
private readonly ISocketCommunicationManager communicationManager;
|
|
||||||
public MoveHandler(
|
|
||||||
ILogger<MoveHandler> 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<MoveRequest>(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;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
74
Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs
Normal file
74
Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs
Normal file
@@ -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<bool> AssignPlayer2ToSession(string sessionName, User user);
|
||||||
|
Task<User?> ReadUser(ClaimsPrincipal user);
|
||||||
|
Task<bool> CreateUser(ClaimsPrincipal user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GameboardManager : IGameboardManager
|
||||||
|
{
|
||||||
|
private readonly IGameboardRepository repository;
|
||||||
|
|
||||||
|
public GameboardManager(IGameboardRepository repository)
|
||||||
|
{
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> 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<User?> ReadUser(ClaimsPrincipal principal)
|
||||||
|
{
|
||||||
|
var userId = principal.UserId();
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return repository.ReadUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<User?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<string> CreateJoinCode(string sessionName, string playerName)
|
||||||
|
{
|
||||||
|
//var session = await repository.GetGame(sessionName);
|
||||||
|
//if (playerName == session?.Player1)
|
||||||
|
//{
|
||||||
|
// return await repository.PostJoinCode(sessionName, playerName);
|
||||||
|
//}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string, WebSocket, string> 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<string, WebSocket> connections;
|
|
||||||
private readonly ConcurrentDictionary<string, List<string>> gameSeats;
|
|
||||||
private readonly ILogger<SocketCommunicationManager> logger;
|
|
||||||
private readonly ActionHandlerResolver handlerResolver;
|
|
||||||
|
|
||||||
public SocketCommunicationManager(
|
|
||||||
ILogger<SocketCommunicationManager> logger,
|
|
||||||
ActionHandlerResolver handlerResolver)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.handlerResolver = handlerResolver;
|
|
||||||
connections = new ConcurrentDictionary<string, WebSocket>();
|
|
||||||
gameSeats = new ConcurrentDictionary<string, List<string>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Request>(message);
|
|
||||||
if (!Enum.IsDefined(typeof(ClientAction), request.Action))
|
|
||||||
{
|
|
||||||
await socket.SendTextAsync("Error: Action not recognized.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var handler = handlerResolver(request.Action);
|
|
||||||
await handler.Handle(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unsubscribes the player from their current game, then subscribes to the new game.
|
|
||||||
/// </summary>
|
|
||||||
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<string> { 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<string, WebSocket, string> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
using System.Net;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.WebSockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Managers
|
namespace Gameboard.ShogiUI.Sockets.Managers
|
||||||
{
|
{
|
||||||
public interface ISocketConnectionManager
|
public interface ISocketConnectionManager
|
||||||
{
|
{
|
||||||
Task HandleSocketRequest(HttpContext context);
|
Task BroadcastToAll(IResponse response);
|
||||||
}
|
//Task BroadcastToGame(string gameName, IResponse response);
|
||||||
|
//Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2);
|
||||||
|
void SubscribeToGame(Session session, string playerName);
|
||||||
|
void SubscribeToBroadcast(WebSocket socket, string playerName);
|
||||||
|
void UnsubscribeFromBroadcastAndGames(string playerName);
|
||||||
|
void UnsubscribeFromGame(string gameName, string playerName);
|
||||||
|
Task BroadcastToPlayers(IResponse response, params string?[] playerNames);
|
||||||
|
}
|
||||||
|
|
||||||
public class SocketConnectionManager : ISocketConnectionManager
|
/// <summary>
|
||||||
{
|
/// Retains all active socket connections and provides convenient methods for sending messages to clients.
|
||||||
private readonly ISocketCommunicationManager communicationManager;
|
/// </summary>
|
||||||
private readonly ISocketTokenManager tokenManager;
|
public class SocketConnectionManager : ISocketConnectionManager
|
||||||
|
{
|
||||||
|
/// <summary>Dictionary key is player name.</summary>
|
||||||
|
private readonly ConcurrentDictionary<string, WebSocket> connections;
|
||||||
|
/// <summary>Dictionary key is game name.</summary>
|
||||||
|
private readonly ConcurrentDictionary<string, Session> sessions;
|
||||||
|
private readonly ILogger<SocketConnectionManager> logger;
|
||||||
|
|
||||||
public SocketConnectionManager(ISocketCommunicationManager communicationManager, ISocketTokenManager tokenManager) : base()
|
public SocketConnectionManager(ILogger<SocketConnectionManager> logger)
|
||||||
{
|
{
|
||||||
this.communicationManager = communicationManager;
|
this.logger = logger;
|
||||||
this.tokenManager = tokenManager;
|
connections = new ConcurrentDictionary<string, WebSocket>();
|
||||||
|
sessions = new ConcurrentDictionary<string, Session>();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
public void SubscribeToBroadcast(WebSocket socket, string playerName)
|
||||||
|
{
|
||||||
|
connections.TryRemove(playerName, out var _);
|
||||||
|
connections.TryAdd(playerName, socket);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task HandleSocketRequest(HttpContext context)
|
public void UnsubscribeFromBroadcastAndGames(string playerName)
|
||||||
{
|
{
|
||||||
var hasToken = context.Request.Query.Keys.Contains("token");
|
connections.TryRemove(playerName, out _);
|
||||||
if (hasToken)
|
foreach (var kvp in sessions)
|
||||||
{
|
{
|
||||||
var oneTimeToken = context.Request.Query["token"][0];
|
var sessionName = kvp.Key;
|
||||||
var tokenAsGuid = Guid.Parse(oneTimeToken);
|
UnsubscribeFromGame(sessionName, playerName);
|
||||||
var userName = tokenManager.GetUsername(tokenAsGuid);
|
}
|
||||||
if (!string.IsNullOrEmpty(userName))
|
}
|
||||||
{
|
|
||||||
var socket = await context.WebSockets.AcceptWebSocketAsync();
|
/// <summary>
|
||||||
await communicationManager.CommunicateWith(socket, userName);
|
/// Unsubscribes the player from their current game, then subscribes to the new game.
|
||||||
return;
|
/// </summary>
|
||||||
}
|
public void SubscribeToGame(Session session, string playerName)
|
||||||
}
|
{
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
// Unsubscribe from any other games
|
||||||
return;
|
foreach (var kvp in sessions)
|
||||||
}
|
{
|
||||||
}
|
var gameNameKey = kvp.Key;
|
||||||
|
UnsubscribeFromGame(gameNameKey, playerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
if (connections.TryGetValue(playerName, out var socket))
|
||||||
|
{
|
||||||
|
var s = sessions.GetOrAdd(session.Name, session);
|
||||||
|
s.Subscriptions.TryAdd(playerName, socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnsubscribeFromGame(string gameName, string playerName)
|
||||||
|
{
|
||||||
|
if (sessions.TryGetValue(gameName, out var s))
|
||||||
|
{
|
||||||
|
s.Subscriptions.TryRemove(playerName, out _);
|
||||||
|
if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames)
|
||||||
|
{
|
||||||
|
var tasks = new List<Task>(playerNames.Length);
|
||||||
|
foreach (var name in playerNames)
|
||||||
|
{
|
||||||
|
if (!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<Task>(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;
|
||||||
|
//}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs
Normal file
54
Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Key is userName or webSessionId
|
||||||
|
/// </summary>
|
||||||
|
private readonly ConcurrentDictionary<string, Guid> Tokens;
|
||||||
|
|
||||||
|
public SocketTokenCache()
|
||||||
|
{
|
||||||
|
Tokens = new ConcurrentDictionary<string, Guid>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <returns>User name associated to the guid or null.</returns>
|
||||||
|
public string? GetUsername(Guid guid)
|
||||||
|
{
|
||||||
|
var userName = Tokens.FirstOrDefault(kvp => kvp.Value == guid).Key;
|
||||||
|
if (userName != null)
|
||||||
|
{
|
||||||
|
Tokens.Remove(userName, out _);
|
||||||
|
}
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Key is userName
|
|
||||||
/// </summary>
|
|
||||||
private readonly Dictionary<string, Guid> Tokens;
|
|
||||||
|
|
||||||
public SocketTokenManager()
|
|
||||||
{
|
|
||||||
Tokens = new Dictionary<string, Guid>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <returns>User name associated to the guid or null.</returns>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<GameboardTypes.PieceName> pieceFromCaptured = source.PieceFromCaptured switch
|
|
||||||
{
|
|
||||||
"B" => new FSharpOption<GameboardTypes.PieceName>(GameboardTypes.PieceName.Bishop),
|
|
||||||
"G" => new FSharpOption<GameboardTypes.PieceName>(GameboardTypes.PieceName.GoldenGeneral),
|
|
||||||
"K" => new FSharpOption<GameboardTypes.PieceName>(GameboardTypes.PieceName.King),
|
|
||||||
"k" => new FSharpOption<GameboardTypes.PieceName>(GameboardTypes.PieceName.Knight),
|
|
||||||
"L" => new FSharpOption<GameboardTypes.PieceName>(GameboardTypes.PieceName.Lance),
|
|
||||||
"P" => new FSharpOption<GameboardTypes.PieceName>(GameboardTypes.PieceName.Pawn),
|
|
||||||
"R" => new FSharpOption<GameboardTypes.PieceName>(GameboardTypes.PieceName.Rook),
|
|
||||||
"S" => new FSharpOption<GameboardTypes.PieceName>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
63
Gameboard.ShogiUI.Sockets/Models/Move.cs
Normal file
63
Gameboard.ShogiUI.Sockets/Models/Move.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor to represent moving a piece on the Board to another position on the Board.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fromNotation">Position the piece is being moved from.</param>
|
||||||
|
/// <param name="toNotation">Position the piece is being moved to.</param>
|
||||||
|
/// <param name="isPromotion">If the moving piece should be promoted.</param>
|
||||||
|
public Move(string fromNotation, string toNotation, bool isPromotion = false)
|
||||||
|
{
|
||||||
|
From = NotationHelper.FromBoardNotation(fromNotation);
|
||||||
|
To = NotationHelper.FromBoardNotation(toNotation);
|
||||||
|
IsPromotion = isPromotion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor to represent moving a piece from the Hand to the Board.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pieceFromHand">The piece being moved from the Hand to the Board.</param>
|
||||||
|
/// <param name="toNotation">Position the piece is being moved to.</param>
|
||||||
|
/// <param name="isPromotion">If the moving piece should be promoted.</param>
|
||||||
|
public Move(WhichPiece pieceFromHand, string toNotation, bool isPromotion = false)
|
||||||
|
{
|
||||||
|
From = null;
|
||||||
|
PieceFromHand = pieceFromHand;
|
||||||
|
To = 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
95
Gameboard.ShogiUI.Sockets/Models/MoveSets.cs
Normal file
95
Gameboard.ShogiUI.Sockets/Models/MoveSets.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using PathFinding;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.Models
|
||||||
|
{
|
||||||
|
public static class MoveSets
|
||||||
|
{
|
||||||
|
public static readonly List<PathFinding.Move> King = new(8)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.Up),
|
||||||
|
new PathFinding.Move(Direction.Left),
|
||||||
|
new PathFinding.Move(Direction.Right),
|
||||||
|
new PathFinding.Move(Direction.Down),
|
||||||
|
new PathFinding.Move(Direction.UpLeft),
|
||||||
|
new PathFinding.Move(Direction.UpRight),
|
||||||
|
new PathFinding.Move(Direction.DownLeft),
|
||||||
|
new PathFinding.Move(Direction.DownRight)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> Bishop = new(4)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> PromotedBishop = new(8)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.Up),
|
||||||
|
new PathFinding.Move(Direction.Left),
|
||||||
|
new PathFinding.Move(Direction.Right),
|
||||||
|
new PathFinding.Move(Direction.Down),
|
||||||
|
new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> GoldGeneral = new(6)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.Up),
|
||||||
|
new PathFinding.Move(Direction.UpLeft),
|
||||||
|
new PathFinding.Move(Direction.UpRight),
|
||||||
|
new PathFinding.Move(Direction.Left),
|
||||||
|
new PathFinding.Move(Direction.Right),
|
||||||
|
new PathFinding.Move(Direction.Down)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> Knight = new(2)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.KnightLeft),
|
||||||
|
new PathFinding.Move(Direction.KnightRight)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> Lance = new(1)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.Up, Distance.MultiStep),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> Pawn = new(1)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.Up)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> Rook = new(4)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.Up, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.Left, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.Right, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.Down, Distance.MultiStep)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> PromotedRook = new(8)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.Up, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.Left, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.Right, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.Down, Distance.MultiStep),
|
||||||
|
new PathFinding.Move(Direction.UpLeft),
|
||||||
|
new PathFinding.Move(Direction.UpRight),
|
||||||
|
new PathFinding.Move(Direction.DownLeft),
|
||||||
|
new PathFinding.Move(Direction.DownRight)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<PathFinding.Move> SilverGeneral = new(4)
|
||||||
|
{
|
||||||
|
new PathFinding.Move(Direction.Up),
|
||||||
|
new PathFinding.Move(Direction.UpLeft),
|
||||||
|
new PathFinding.Move(Direction.UpRight),
|
||||||
|
new PathFinding.Move(Direction.DownLeft),
|
||||||
|
new PathFinding.Move(Direction.DownRight)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Gameboard.ShogiUI.Sockets/Models/Piece.cs
Normal file
70
Gameboard.ShogiUI.Sockets/Models/Piece.cs
Normal file
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Gameboard.ShogiUI.Sockets/Models/Session.cs
Normal file
38
Gameboard.ShogiUI.Sockets/Models/Session.cs
Normal file
@@ -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<string, WebSocket> Subscriptions { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public User Player1 { get; }
|
||||||
|
public User? Player2 { get; private set; }
|
||||||
|
public bool IsPrivate { get; }
|
||||||
|
|
||||||
|
// TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied.
|
||||||
|
public Shogi Shogi { get; }
|
||||||
|
|
||||||
|
public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null)
|
||||||
|
{
|
||||||
|
Subscriptions = new ConcurrentDictionary<string, WebSocket>();
|
||||||
|
|
||||||
|
Name = name;
|
||||||
|
Player1 = player1;
|
||||||
|
Player2 = player2;
|
||||||
|
IsPrivate = isPrivate;
|
||||||
|
Shogi = shogi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPlayer2(User user)
|
||||||
|
{
|
||||||
|
Player2 = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs
Normal file
37
Gameboard.ShogiUI.Sockets/Models/SessionMetadata.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A representation of a Session without the board and game-rules.
|
||||||
|
/// </summary>
|
||||||
|
public class SessionMetadata
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
463
Gameboard.ShogiUI.Sockets/Models/Shogi.cs
Normal file
463
Gameboard.ShogiUI.Sockets/Models/Shogi.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Facilitates Shogi board state transitions, cognisant of Shogi rules.
|
||||||
|
/// The board is always from Player1's perspective.
|
||||||
|
/// [0,0] is the lower-left position, [8,8] is the higher-right position
|
||||||
|
/// </summary>
|
||||||
|
public class Shogi
|
||||||
|
{
|
||||||
|
private delegate void MoveSetCallback(Piece piece, Vector2 position);
|
||||||
|
private readonly PathFinder2D<Piece> pathFinder;
|
||||||
|
private Shogi? validationBoard;
|
||||||
|
private Vector2 player1King;
|
||||||
|
private Vector2 player2King;
|
||||||
|
private List<Piece> Hand => WhoseTurn == WhichPerspective.Player1 ? Player1Hand : Player2Hand;
|
||||||
|
public List<Piece> Player1Hand { get; }
|
||||||
|
public List<Piece> Player2Hand { get; }
|
||||||
|
public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method
|
||||||
|
public List<Move> MoveHistory { get; }
|
||||||
|
public WhichPerspective WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPerspective.Player1 : WhichPerspective.Player2;
|
||||||
|
public WhichPerspective? InCheck { get; private set; }
|
||||||
|
public bool IsCheckmate { get; private set; }
|
||||||
|
|
||||||
|
public string Error { get; private set; }
|
||||||
|
|
||||||
|
public Shogi()
|
||||||
|
{
|
||||||
|
Board = new CoordsToNotationCollection();
|
||||||
|
MoveHistory = new List<Move>(20);
|
||||||
|
Player1Hand = new List<Piece>();
|
||||||
|
Player2Hand = new List<Piece>();
|
||||||
|
pathFinder = new PathFinder2D<Piece>(Board, 9, 9);
|
||||||
|
player1King = new Vector2(4, 0);
|
||||||
|
player2King = new Vector2(4, 8);
|
||||||
|
Error = string.Empty;
|
||||||
|
|
||||||
|
InitializeBoardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Shogi(IList<Move> moves) : this()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < moves.Count; i++)
|
||||||
|
{
|
||||||
|
if (!Move(moves[i]))
|
||||||
|
{
|
||||||
|
// Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc.
|
||||||
|
throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Shogi(Shogi toCopy)
|
||||||
|
{
|
||||||
|
Board = new CoordsToNotationCollection();
|
||||||
|
foreach (var kvp in toCopy.Board)
|
||||||
|
{
|
||||||
|
Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pathFinder = new PathFinder2D<Piece>(Board, 9, 9);
|
||||||
|
MoveHistory = new List<Move>(toCopy.MoveHistory);
|
||||||
|
Player1Hand = new List<Piece>(toCopy.Player1Hand);
|
||||||
|
Player2Hand = new List<Piece>(toCopy.Player2Hand);
|
||||||
|
player1King = toCopy.player1King;
|
||||||
|
player2King = toCopy.player2King;
|
||||||
|
Error = toCopy.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Move(Move move)
|
||||||
|
{
|
||||||
|
var otherPlayer = WhoseTurn == WhichPerspective.Player1 ? WhichPerspective.Player2 : WhichPerspective.Player1;
|
||||||
|
var moveSuccess = TryMove(move);
|
||||||
|
|
||||||
|
if (!moveSuccess)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate check
|
||||||
|
if (EvaluateCheckAfterMove(move, otherPlayer))
|
||||||
|
{
|
||||||
|
InCheck = otherPlayer;
|
||||||
|
IsCheckmate = EvaluateCheckmate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InCheck = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts a given move. Returns false if the move is illegal.
|
||||||
|
/// </summary>
|
||||||
|
private bool TryMove(Move move)
|
||||||
|
{
|
||||||
|
// Try making the move in a "throw away" board.
|
||||||
|
if (validationBoard == null)
|
||||||
|
{
|
||||||
|
validationBoard = new Shogi(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
var isValid = move.PieceFromHand.HasValue
|
||||||
|
? validationBoard.PlaceFromHand(move)
|
||||||
|
: validationBoard.PlaceFromBoard(move);
|
||||||
|
if (!isValid)
|
||||||
|
{
|
||||||
|
// Surface the error description.
|
||||||
|
Error = validationBoard.Error;
|
||||||
|
// Invalidate the "throw away" board.
|
||||||
|
validationBoard = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If already in check, assert the move that resulted in check no longer results in check.
|
||||||
|
if (InCheck == WhoseTurn)
|
||||||
|
{
|
||||||
|
if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn))
|
||||||
|
{
|
||||||
|
// Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The move is valid and legal; update board state.
|
||||||
|
if (move.PieceFromHand.HasValue) PlaceFromHand(move);
|
||||||
|
else PlaceFromBoard(move);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/// <returns>True if the move was successful.</returns>
|
||||||
|
private bool PlaceFromHand(Move move)
|
||||||
|
{
|
||||||
|
var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
Error = $"{move.PieceFromHand} does not exist in the hand.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Board[move.To] != null)
|
||||||
|
{
|
||||||
|
Error = $"Illegal move - attempting to capture while playing a piece from the hand.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (move.PieceFromHand!.Value)
|
||||||
|
{
|
||||||
|
case WhichPiece.Knight:
|
||||||
|
{
|
||||||
|
// Knight cannot be placed onto the farthest two ranks from the hand.
|
||||||
|
if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y > 6)
|
||||||
|
|| (WhoseTurn == WhichPerspective.Player2 && move.To.Y < 2))
|
||||||
|
{
|
||||||
|
Error = $"Knight has no valid moves after placed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WhichPiece.Lance:
|
||||||
|
case WhichPiece.Pawn:
|
||||||
|
{
|
||||||
|
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
|
||||||
|
if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y == 8)
|
||||||
|
|| (WhoseTurn == WhichPerspective.Player2 && move.To.Y == 0))
|
||||||
|
{
|
||||||
|
Error = $"{move.PieceFromHand} has no valid moves after placed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutate the board.
|
||||||
|
Board[move.To] = Hand[index];
|
||||||
|
Hand.RemoveAt(index);
|
||||||
|
MoveHistory.Add(move);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/// <returns>True if the move was successful.</returns>
|
||||||
|
private bool PlaceFromBoard(Move move)
|
||||||
|
{
|
||||||
|
var fromPiece = Board[move.From!.Value];
|
||||||
|
if (fromPiece == null)
|
||||||
|
{
|
||||||
|
Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}.";
|
||||||
|
return false; // Invalid move
|
||||||
|
}
|
||||||
|
if (fromPiece.Owner != WhoseTurn)
|
||||||
|
{
|
||||||
|
Error = "Not allowed to move the opponents piece";
|
||||||
|
return false; // Invalid move; cannot move other players pieces.
|
||||||
|
}
|
||||||
|
if (IsPathable(move.From.Value, move.To) == false)
|
||||||
|
{
|
||||||
|
Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set.";
|
||||||
|
return false; // Invalid move; move not part of move-set.
|
||||||
|
}
|
||||||
|
|
||||||
|
var captured = Board[move.To];
|
||||||
|
if (captured != null)
|
||||||
|
{
|
||||||
|
if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece.
|
||||||
|
captured.Capture();
|
||||||
|
Hand.Add(captured);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Mutate the board.
|
||||||
|
if (move.IsPromotion)
|
||||||
|
{
|
||||||
|
if (WhoseTurn == WhichPerspective.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5))
|
||||||
|
{
|
||||||
|
fromPiece.Promote();
|
||||||
|
}
|
||||||
|
else if (WhoseTurn == WhichPerspective.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3))
|
||||||
|
{
|
||||||
|
fromPiece.Promote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Board[move.To] = fromPiece;
|
||||||
|
Board[move.From!.Value] = null;
|
||||||
|
if (fromPiece.WhichPiece == WhichPiece.King)
|
||||||
|
{
|
||||||
|
if (fromPiece.Owner == WhichPerspective.Player1)
|
||||||
|
{
|
||||||
|
player1King.X = move.To.X;
|
||||||
|
player1King.Y = move.To.Y;
|
||||||
|
}
|
||||||
|
else if (fromPiece.Owner == WhichPerspective.Player2)
|
||||||
|
{
|
||||||
|
player2King.X = move.To.X;
|
||||||
|
player2King.Y = move.To.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MoveHistory.Add(move);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPathable(Vector2 from, Vector2 to)
|
||||||
|
{
|
||||||
|
var piece = Board[from];
|
||||||
|
if (piece == null) return false;
|
||||||
|
|
||||||
|
var isObstructed = false;
|
||||||
|
var isPathable = pathFinder.PathTo(from, to, (other, position) =>
|
||||||
|
{
|
||||||
|
if (other.Owner == piece.Owner) isObstructed = true;
|
||||||
|
});
|
||||||
|
return !isObstructed && isPathable;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Rules Validation
|
||||||
|
private bool EvaluateCheckAfterMove(Move move, WhichPerspective WhichPerspective)
|
||||||
|
{
|
||||||
|
if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother.
|
||||||
|
|
||||||
|
var isCheck = false;
|
||||||
|
var kingPosition = WhichPerspective == WhichPerspective.Player1 ? player1King : player2King;
|
||||||
|
|
||||||
|
// Check if the move put the king in check.
|
||||||
|
if (pathFinder.PathTo(move.To, kingPosition)) return true;
|
||||||
|
|
||||||
|
if (move.From.HasValue)
|
||||||
|
{
|
||||||
|
// Get line equation from king through the now-unoccupied location.
|
||||||
|
var direction = Vector2.Subtract(kingPosition, move.From!.Value);
|
||||||
|
var slope = Math.Abs(direction.Y / direction.X);
|
||||||
|
// If absolute slope is 45°, look for a bishop along the line.
|
||||||
|
// If absolute slope is 0° or 90°, look for a rook along the line.
|
||||||
|
// if absolute slope is 0°, look for lance along the line.
|
||||||
|
if (float.IsInfinity(slope))
|
||||||
|
{
|
||||||
|
// if slope of the move is also infinity...can skip this?
|
||||||
|
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
|
||||||
|
{
|
||||||
|
if (piece.Owner != WhichPerspective)
|
||||||
|
{
|
||||||
|
switch (piece.WhichPiece)
|
||||||
|
{
|
||||||
|
case WhichPiece.Rook:
|
||||||
|
isCheck = true;
|
||||||
|
break;
|
||||||
|
case WhichPiece.Lance:
|
||||||
|
if (!piece.IsPromoted) isCheck = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (slope == 1)
|
||||||
|
{
|
||||||
|
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
|
||||||
|
{
|
||||||
|
if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop)
|
||||||
|
{
|
||||||
|
isCheck = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (slope == 0)
|
||||||
|
{
|
||||||
|
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
|
||||||
|
{
|
||||||
|
if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook)
|
||||||
|
{
|
||||||
|
isCheck = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent.
|
||||||
|
// Go read the shogi rules to be sure this is true.
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCheck;
|
||||||
|
}
|
||||||
|
private bool EvaluateCheckmate()
|
||||||
|
{
|
||||||
|
if (!InCheck.HasValue) return false;
|
||||||
|
|
||||||
|
// Assume true and try to disprove.
|
||||||
|
var isCheckmate = true;
|
||||||
|
Board.ForEachNotNull((piece, from) => // For each piece...
|
||||||
|
{
|
||||||
|
// Short circuit
|
||||||
|
if (!isCheckmate) return;
|
||||||
|
|
||||||
|
if (piece.Owner == InCheck) // ...owned by the player in check...
|
||||||
|
{
|
||||||
|
// ...evaluate if any move gets the player out of check.
|
||||||
|
pathFinder.PathEvery(from, (other, position) =>
|
||||||
|
{
|
||||||
|
if (validationBoard == null) validationBoard = new Shogi(this);
|
||||||
|
var moveToTry = new Move(from, position);
|
||||||
|
var moveSuccess = validationBoard.TryMove(moveToTry);
|
||||||
|
if (moveSuccess)
|
||||||
|
{
|
||||||
|
validationBoard = null;
|
||||||
|
if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value))
|
||||||
|
{
|
||||||
|
isCheckmate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return isCheckmate;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void InitializeBoardState()
|
||||||
|
{
|
||||||
|
Board["A1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1);
|
||||||
|
Board["B1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1);
|
||||||
|
Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1);
|
||||||
|
Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1);
|
||||||
|
Board["E1"] = new Piece(WhichPiece.King, WhichPerspective.Player1);
|
||||||
|
Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1);
|
||||||
|
Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1);
|
||||||
|
Board["H1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1);
|
||||||
|
Board["I1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1);
|
||||||
|
|
||||||
|
Board["A2"] = null;
|
||||||
|
Board["B2"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player1);
|
||||||
|
Board["C2"] = null;
|
||||||
|
Board["D2"] = null;
|
||||||
|
Board["E2"] = null;
|
||||||
|
Board["F2"] = null;
|
||||||
|
Board["G2"] = null;
|
||||||
|
Board["H2"] = new Piece(WhichPiece.Rook, WhichPerspective.Player1);
|
||||||
|
Board["I2"] = null;
|
||||||
|
|
||||||
|
Board["A3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
Board["B3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
Board["C3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
Board["D3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
Board["E3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
Board["F3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
Board["G3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
Board["H3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
Board["I3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
|
||||||
|
|
||||||
|
Board["A4"] = null;
|
||||||
|
Board["B4"] = null;
|
||||||
|
Board["C4"] = null;
|
||||||
|
Board["D4"] = null;
|
||||||
|
Board["E4"] = null;
|
||||||
|
Board["F4"] = null;
|
||||||
|
Board["G4"] = null;
|
||||||
|
Board["H4"] = null;
|
||||||
|
Board["I4"] = null;
|
||||||
|
|
||||||
|
Board["A5"] = null;
|
||||||
|
Board["B5"] = null;
|
||||||
|
Board["C5"] = null;
|
||||||
|
Board["D5"] = null;
|
||||||
|
Board["E5"] = null;
|
||||||
|
Board["F5"] = null;
|
||||||
|
Board["G5"] = null;
|
||||||
|
Board["H5"] = null;
|
||||||
|
Board["I5"] = null;
|
||||||
|
|
||||||
|
Board["A6"] = null;
|
||||||
|
Board["B6"] = null;
|
||||||
|
Board["C6"] = null;
|
||||||
|
Board["D6"] = null;
|
||||||
|
Board["E6"] = null;
|
||||||
|
Board["F6"] = null;
|
||||||
|
Board["G6"] = null;
|
||||||
|
Board["H6"] = null;
|
||||||
|
Board["I6"] = null;
|
||||||
|
|
||||||
|
Board["A7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
Board["B7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
Board["C7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
Board["D7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
Board["E7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
Board["F7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
Board["G7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
Board["H7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
Board["I7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
|
||||||
|
|
||||||
|
Board["A8"] = null;
|
||||||
|
Board["B8"] = new Piece(WhichPiece.Rook, WhichPerspective.Player2);
|
||||||
|
Board["C8"] = null;
|
||||||
|
Board["D8"] = null;
|
||||||
|
Board["E8"] = null;
|
||||||
|
Board["F8"] = null;
|
||||||
|
Board["G8"] = null;
|
||||||
|
Board["H8"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player2);
|
||||||
|
Board["I8"] = null;
|
||||||
|
|
||||||
|
Board["A9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2);
|
||||||
|
Board["B9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2);
|
||||||
|
Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2);
|
||||||
|
Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2);
|
||||||
|
Board["E9"] = new Piece(WhichPiece.King, WhichPerspective.Player2);
|
||||||
|
Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2);
|
||||||
|
Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2);
|
||||||
|
Board["H9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2);
|
||||||
|
Board["I9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoardState ToServiceModel()
|
||||||
|
{
|
||||||
|
return new BoardState
|
||||||
|
{
|
||||||
|
Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()),
|
||||||
|
PlayerInCheck = InCheck,
|
||||||
|
WhoseTurn = WhoseTurn,
|
||||||
|
Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(),
|
||||||
|
Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
Gameboard.ShogiUI.Sockets/Models/User.cs
Normal file
87
Gameboard.ShogiUI.Sockets/Models/User.cs
Normal file
@@ -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<string> Adjectives = new(new[] {
|
||||||
|
"Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly"
|
||||||
|
});
|
||||||
|
public static readonly ReadOnlyCollection<string> Subjects = new(new[] {
|
||||||
|
"Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato", "Penguin"
|
||||||
|
});
|
||||||
|
public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft);
|
||||||
|
public static User CreateGuestUser(string id)
|
||||||
|
{
|
||||||
|
var random = new Random();
|
||||||
|
// Adjective
|
||||||
|
var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count);
|
||||||
|
var adj = Adjectives[index];
|
||||||
|
// Subject
|
||||||
|
index = (int)Math.Floor(random.NextDouble() * Subjects.Count);
|
||||||
|
var subj = Subjects[index];
|
||||||
|
|
||||||
|
return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; }
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
public WhichLoginPlatform LoginPlatform { get; }
|
||||||
|
|
||||||
|
public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest;
|
||||||
|
|
||||||
|
public bool IsAdmin => LoginPlatform == WhichLoginPlatform.Microsoft && Id == "Hauth@live.com";
|
||||||
|
|
||||||
|
public User(string id, string displayName, WhichLoginPlatform platform)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
DisplayName = displayName;
|
||||||
|
LoginPlatform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User(UserDocument document)
|
||||||
|
{
|
||||||
|
Id = document.Id;
|
||||||
|
DisplayName = document.DisplayName;
|
||||||
|
LoginPlatform = document.Platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimsIdentity CreateClaimsIdentity()
|
||||||
|
{
|
||||||
|
if (LoginPlatform == WhichLoginPlatform.Guest)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>(4)
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, Id),
|
||||||
|
new Claim(ClaimTypes.Name, DisplayName),
|
||||||
|
new Claim(ClaimTypes.Role, "Guest"),
|
||||||
|
new Claim(ClaimTypes.Role, "Shogi") // The Shogi role grants access to api controllers.
|
||||||
|
};
|
||||||
|
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>(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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
Normal file
9
Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.Models
|
||||||
|
{
|
||||||
|
public enum WhichLoginPlatform
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Microsoft,
|
||||||
|
Guest
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
"iisSettings": {
|
"iisSettings": {
|
||||||
"windowsAuthentication": false,
|
"windowsAuthentication": false,
|
||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:63676",
|
"applicationUrl": "http://localhost:50728/",
|
||||||
"sslPort": 44396
|
"sslPort": 44315
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
@@ -16,13 +16,14 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AspShogiSockets": {
|
"Kestrel": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchUrl": "Socket/Token",
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "/swagger",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"applicationUrl": "http://127.0.0.1:5100"
|
"applicationUrl": "http://localhost:5100"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A dictionary where the key is a board-notation position, like D3.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, Piece?> Board { get; set; }
|
||||||
|
|
||||||
|
public Piece[] Player1Hand { get; set; }
|
||||||
|
|
||||||
|
public Piece[] Player2Hand { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move is null for first BoardState of a session - before anybody has made moves.
|
||||||
|
/// </summary>
|
||||||
|
public Move? Move { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default constructor and setters are for deserialization.
|
||||||
|
/// </summary>
|
||||||
|
public BoardStateDocument() : base(WhichDocumentType.BoardState)
|
||||||
|
{
|
||||||
|
Name = string.Empty;
|
||||||
|
Board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
|
||||||
|
Player1Hand = Array.Empty<Piece>();
|
||||||
|
Player2Hand = Array.Empty<Piece>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoardStateDocument(string sessionName, Models.Shogi shogi)
|
||||||
|
: base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState)
|
||||||
|
{
|
||||||
|
Name = sessionName;
|
||||||
|
Board = new Dictionary<string, Piece?>(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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
|
{
|
||||||
|
internal class CouchFindResult<T>
|
||||||
|
{
|
||||||
|
public T[] docs;
|
||||||
|
public string warning;
|
||||||
|
|
||||||
|
public CouchFindResult()
|
||||||
|
{
|
||||||
|
docs = Array.Empty<T>();
|
||||||
|
warning = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
|
{
|
||||||
|
public class CouchViewResult<T> where T : class
|
||||||
|
{
|
||||||
|
public int total_rows;
|
||||||
|
public int offset;
|
||||||
|
public CouchViewResultRow<T>[] rows;
|
||||||
|
|
||||||
|
public CouchViewResult()
|
||||||
|
{
|
||||||
|
rows = Array.Empty<CouchViewResultRow<T>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CouchViewResultRow<T>
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
public T doc;
|
||||||
|
|
||||||
|
public CouchViewResultRow()
|
||||||
|
{
|
||||||
|
id = string.Empty;
|
||||||
|
doc = default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs
Normal file
56
Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
|
{
|
||||||
|
public class Move
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A board coordinate, like A3 or G6. When null, look for PieceFromHand to exist.
|
||||||
|
/// </summary>
|
||||||
|
public string? From { get; set; }
|
||||||
|
|
||||||
|
public bool IsPromotion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The piece placed from the player's hand.
|
||||||
|
/// </summary>
|
||||||
|
public WhichPiece? PieceFromHand { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A board coordinate, like A3 or G6.
|
||||||
|
/// </summary>
|
||||||
|
public string To { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default constructor and setters are for deserialization.
|
||||||
|
/// </summary>
|
||||||
|
public Move()
|
||||||
|
{
|
||||||
|
To = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Move(Models.Move move)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs
Normal file
27
Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs
Normal file
@@ -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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default constructor and setters are for deserialization.
|
||||||
|
/// </summary>
|
||||||
|
public Piece()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Piece(Models.Piece piece)
|
||||||
|
{
|
||||||
|
IsPromoted = piece.IsPromoted;
|
||||||
|
Owner = piece.Owner;
|
||||||
|
WhichPiece = piece.WhichPiece;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Models.Piece ToDomainModel() => new(WhichPiece, Owner, IsPromoted);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BoardStateDocument> History { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default constructor and setters are for deserialization.
|
||||||
|
/// </summary>
|
||||||
|
public SessionDocument() : base(WhichDocumentType.Session)
|
||||||
|
{
|
||||||
|
Name = string.Empty;
|
||||||
|
Player1Id = string.Empty;
|
||||||
|
Player2Id = string.Empty;
|
||||||
|
History = new List<BoardStateDocument>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionDocument(Models.Session session)
|
||||||
|
: base(session.Name, WhichDocumentType.Session)
|
||||||
|
{
|
||||||
|
Name = session.Name;
|
||||||
|
Player1Id = session.Player1.Id;
|
||||||
|
Player2Id = session.Player2?.Id;
|
||||||
|
IsPrivate = session.IsPrivate;
|
||||||
|
History = new List<BoardStateDocument>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionDocument(Models.SessionMetadata sessionMetaData)
|
||||||
|
: base(sessionMetaData.Name, WhichDocumentType.Session)
|
||||||
|
{
|
||||||
|
Name = sessionMetaData.Name;
|
||||||
|
Player1Id = sessionMetaData.Player1.Id;
|
||||||
|
Player2Id = sessionMetaData.Player2?.Id;
|
||||||
|
IsPrivate = sessionMetaData.IsPrivate;
|
||||||
|
History = new List<BoardStateDocument>(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for JSON deserializing.
|
||||||
|
/// </summary>
|
||||||
|
public UserDocument() : base(WhichDocumentType.User)
|
||||||
|
{
|
||||||
|
DisplayName = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserDocument(
|
||||||
|
string id,
|
||||||
|
string displayName,
|
||||||
|
WhichLoginPlatform platform) : base(id, WhichDocumentType.User)
|
||||||
|
{
|
||||||
|
DisplayName = displayName;
|
||||||
|
Platform = platform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
|
||||||
|
{
|
||||||
|
public enum WhichDocumentType
|
||||||
|
{
|
||||||
|
User,
|
||||||
|
Session,
|
||||||
|
BoardState
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,128 +1,292 @@
|
|||||||
using Gameboard.Shogi.Api.ServiceModels.Messages;
|
using Gameboard.ShogiUI.Sockets.Extensions;
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories.Utility;
|
using Gameboard.ShogiUI.Sockets.Repositories.CouchModels;
|
||||||
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets.Repositories
|
namespace Gameboard.ShogiUI.Sockets.Repositories
|
||||||
{
|
{
|
||||||
public interface IGameboardRepository
|
public interface IGameboardRepository
|
||||||
{
|
{
|
||||||
Task DeleteGame(string gameName);
|
Task<bool> CreateBoardState(Models.Session session);
|
||||||
Task<GetSessionResponse> GetGame(string gameName);
|
Task<bool> CreateSession(Models.SessionMetadata session);
|
||||||
Task<GetSessionsResponse> GetGames();
|
Task<bool> CreateUser(Models.User user);
|
||||||
Task<GetSessionsResponse> GetGames(string playerName);
|
Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas();
|
||||||
Task<GetMovesResponse> GetMoves(string gameName);
|
Task<Models.Session?> ReadSession(string name);
|
||||||
Task<PostSessionResponse> PostSession(PostSession request);
|
Task<bool> UpdateSession(Models.SessionMetadata session);
|
||||||
Task<PostJoinPrivateSessionResponse> PostJoinPrivateSession(PostJoinPrivateSession request);
|
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
|
||||||
Task<PutJoinPublicSessionResponse> PutJoinPublicSession(string gameName, PutJoinPublicSession request);
|
Task<Models.User?> ReadUser(string userName);
|
||||||
Task PostMove(string gameName, PostMove request);
|
|
||||||
Task<PostJoinCodeResponse> PostJoinCode(string gameName, string userName);
|
|
||||||
Task<GetPlayerResponse> GetPlayer(string userName);
|
|
||||||
Task<HttpResponseMessage> PostPlayer(PostPlayer request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GameboardRepository : IGameboardRepository
|
public class GameboardRepository : IGameboardRepository
|
||||||
{
|
{
|
||||||
private readonly IAuthenticatedHttpClient client;
|
/// <summary>
|
||||||
public GameboardRepository(IAuthenticatedHttpClient client)
|
/// Returns session, board state, and user documents, grouped by session.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate";
|
||||||
|
/// <summary>
|
||||||
|
/// Returns session and user documents, grouped by session.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata";
|
||||||
|
private static readonly string View_User = "_design/user/_view/user";
|
||||||
|
private const string ApplicationJson = "application/json";
|
||||||
|
private readonly HttpClient client;
|
||||||
|
private readonly ILogger<GameboardRepository> logger;
|
||||||
|
|
||||||
|
public GameboardRepository(IHttpClientFactory clientFactory, ILogger<GameboardRepository> logger)
|
||||||
{
|
{
|
||||||
this.client = client;
|
client = clientFactory.CreateClient("couchdb");
|
||||||
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetSessionsResponse> GetGames()
|
public async Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas()
|
||||||
{
|
{
|
||||||
var response = await client.GetAsync("Sessions");
|
var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
|
||||||
return JsonConvert.DeserializeObject<GetSessionsResponse>(json);
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
var groupedBySession = result.rows.GroupBy(row => row.id);
|
||||||
|
var sessions = new List<Models.SessionMetadata>(result.total_rows / 3);
|
||||||
|
foreach (var group in groupedBySession)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A group contains 3 elements.
|
||||||
|
* 1) The session metadata.
|
||||||
|
* 2) User document of Player1.
|
||||||
|
* 3) User document of Player2.
|
||||||
|
*/
|
||||||
|
var session = group.FirstOrDefault()?.doc.ToObject<SessionDocument>();
|
||||||
|
var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject<UserDocument>();
|
||||||
|
var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject<UserDocument>();
|
||||||
|
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<Models.SessionMetadata>(sessions);
|
||||||
|
}
|
||||||
|
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetSessionsResponse> GetGames(string playerName)
|
public async Task<Models.Session?> ReadSession(string name)
|
||||||
{
|
{
|
||||||
var uri = $"Sessions/{playerName}";
|
var queryParams = new QueryBuilder
|
||||||
var response = await client.GetAsync(Uri.EscapeUriString(uri));
|
{
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
{ "include_docs", "true" },
|
||||||
return JsonConvert.DeserializeObject<GetSessionsResponse>(json);
|
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
|
||||||
|
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
|
||||||
|
}.ToQueryString();
|
||||||
|
var query = $"{View_SessionWithBoardState}{queryParams}";
|
||||||
|
logger.LogInformation("ReadSession() query: {query}", query);
|
||||||
|
var response = await client.GetAsync(query);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
||||||
|
if (result != null && result.rows.Length > 2)
|
||||||
|
{
|
||||||
|
var group = result.rows;
|
||||||
|
/**
|
||||||
|
* A group contains 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<SessionDocument>();
|
||||||
|
var player1Doc = group[1].doc.ToObject<UserDocument>();
|
||||||
|
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
|
||||||
|
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
|
||||||
|
? group[2].doc.ToObject<UserDocument>()
|
||||||
|
: 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<BoardStateDocument>())
|
||||||
|
.Select(boardState =>
|
||||||
|
{
|
||||||
|
var move = boardState!.Move!;
|
||||||
|
return move.PieceFromHand.HasValue
|
||||||
|
? new Models.Move(move.PieceFromHand.Value, move.To)
|
||||||
|
: new Models.Move(move.From!, move.To, move.IsPromotion);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var shogi = new Models.Shogi(moves);
|
||||||
|
if (session != null && player1Doc != null)
|
||||||
|
{
|
||||||
|
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<GetSessionResponse> GetGame(string gameName)
|
public async Task<Models.SessionMetadata?> ReadSessionMetaData(string name)
|
||||||
{
|
{
|
||||||
var uri = $"Session/{gameName}";
|
var queryParams = new QueryBuilder
|
||||||
var response = await client.GetAsync(Uri.EscapeUriString(uri));
|
{
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
{ "include_docs", "true" },
|
||||||
return JsonConvert.DeserializeObject<GetSessionResponse>(json);
|
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
|
||||||
|
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
|
||||||
|
}.ToQueryString();
|
||||||
|
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
|
||||||
|
if (result != null && result.rows.Length > 2)
|
||||||
|
{
|
||||||
|
var group = result.rows;
|
||||||
|
/**
|
||||||
|
* A group contains 3 elements.
|
||||||
|
* 1) The session metadata.
|
||||||
|
* 2) User document of Player1.
|
||||||
|
* 3) User document of Player2.
|
||||||
|
*/
|
||||||
|
var session = group[0].doc.ToObject<SessionDocument>();
|
||||||
|
var player1Doc = group[1].doc.ToObject<UserDocument>();
|
||||||
|
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
|
||||||
|
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
|
||||||
|
? group[2].doc.ToObject<UserDocument>()
|
||||||
|
: 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)
|
/// <summary>
|
||||||
|
/// Saves a snapshot of board state and the most recent move.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> CreateBoardState(Models.Session session)
|
||||||
{
|
{
|
||||||
var uri = $"Session/{gameName}";
|
var boardStateDocument = new BoardStateDocument(session.Name, session.Shogi);
|
||||||
await client.DeleteAsync(Uri.EscapeUriString(uri));
|
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
|
||||||
|
var response = await client.PostAsync(string.Empty, content);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PostSessionResponse> PostSession(PostSession request)
|
public async Task<bool> CreateSession(Models.SessionMetadata session)
|
||||||
{
|
{
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
|
var sessionDocument = new SessionDocument(session);
|
||||||
var response = await client.PostAsync("Session", content);
|
var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent);
|
||||||
return JsonConvert.DeserializeObject<PostSessionResponse>(json);
|
|
||||||
|
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<PutJoinPublicSessionResponse> PutJoinPublicSession(string gameName, PutJoinPublicSession request)
|
public async Task<bool> UpdateSession(Models.SessionMetadata session)
|
||||||
{
|
{
|
||||||
var uri = $"Session/Join";
|
// GET existing session to get revisionId.
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
|
var readResponse = await client.GetAsync(session.Name);
|
||||||
var response = await client.PutAsync(Uri.EscapeUriString(uri), content);
|
if (!readResponse.IsSuccessStatusCode) return false;
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var sessionDocument = JsonConvert.DeserializeObject<SessionDocument>(await readResponse.Content.ReadAsStringAsync());
|
||||||
return JsonConvert.DeserializeObject<PutJoinPublicSessionResponse>(json);
|
|
||||||
|
// 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<bool> PutJoinPublicSession(PutJoinPublicSession request)
|
||||||
|
//{
|
||||||
|
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
|
||||||
|
// var response = await client.PutAsync(JoinSessionRoute, content);
|
||||||
|
// var json = await response.Content.ReadAsStringAsync();
|
||||||
|
// return JsonConvert.DeserializeObject<PutJoinPublicSessionResponse>(json).JoinSucceeded;
|
||||||
|
//}
|
||||||
|
|
||||||
|
//public async Task<string> PostJoinPrivateSession(PostJoinPrivateSession request)
|
||||||
|
//{
|
||||||
|
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
|
||||||
|
// var response = await client.PostAsync(JoinSessionRoute, content);
|
||||||
|
// var json = await response.Content.ReadAsStringAsync();
|
||||||
|
// var deserialized = JsonConvert.DeserializeObject<PostJoinPrivateSessionResponse>(json);
|
||||||
|
// if (deserialized.JoinSucceeded)
|
||||||
|
// {
|
||||||
|
// return deserialized.SessionName;
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
//}
|
||||||
|
|
||||||
|
//public async Task<List<Move>> GetMoves(string gameName)
|
||||||
|
//{
|
||||||
|
// var uri = $"Session/{gameName}/Moves";
|
||||||
|
// var get = await client.GetAsync(Uri.EscapeUriString(uri));
|
||||||
|
// var json = await get.Content.ReadAsStringAsync();
|
||||||
|
// if (string.IsNullOrWhiteSpace(json))
|
||||||
|
// {
|
||||||
|
// return new List<Move>();
|
||||||
|
// }
|
||||||
|
// var response = JsonConvert.DeserializeObject<GetMovesResponse>(json);
|
||||||
|
// return response.Moves.Select(m => new Move(m)).ToList();
|
||||||
|
//}
|
||||||
|
|
||||||
|
//public async Task PostMove(string gameName, PostMove request)
|
||||||
|
//{
|
||||||
|
// var uri = $"Session/{gameName}/Move";
|
||||||
|
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
|
||||||
|
// await client.PostAsync(Uri.EscapeUriString(uri), content);
|
||||||
|
//}
|
||||||
|
|
||||||
|
public async Task<string> PostJoinCode(string gameName, string userName)
|
||||||
|
{
|
||||||
|
// var uri = $"JoinCode/{gameName}";
|
||||||
|
// var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName });
|
||||||
|
// var content = new StringContent(serialized, Encoding.UTF8, MediaType);
|
||||||
|
// var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync();
|
||||||
|
// return JsonConvert.DeserializeObject<PostJoinCodeResponse>(json).JoinCode;
|
||||||
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PostJoinPrivateSessionResponse> PostJoinPrivateSession(PostJoinPrivateSession request)
|
public async Task<Models.User?> ReadUser(string id)
|
||||||
{
|
{
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
|
var queryParams = new QueryBuilder
|
||||||
var response = await client.PostAsync("Session/Join", content);
|
{
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
{ "include_docs", "true" },
|
||||||
return JsonConvert.DeserializeObject<PostJoinPrivateSessionResponse>(json);
|
{ "key", JsonConvert.SerializeObject(id) },
|
||||||
|
}.ToQueryString();
|
||||||
|
var response = await client.GetAsync($"{View_User}{queryParams}");
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<CouchViewResult<UserDocument>>(responseContent);
|
||||||
|
if (result != null && result.rows.Length > 0)
|
||||||
|
{
|
||||||
|
return new Models.User(result.rows[0].doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetMovesResponse> GetMoves(string gameName)
|
public async Task<bool> CreateUser(Models.User user)
|
||||||
{
|
{
|
||||||
var uri = $"Session/{gameName}/Moves";
|
var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform);
|
||||||
var response = await client.GetAsync(Uri.EscapeUriString(uri));
|
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var response = await client.PostAsync(string.Empty, content);
|
||||||
return JsonConvert.DeserializeObject<GetMovesResponse>(json);
|
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<PostJoinCodeResponse> 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<PostJoinCodeResponse>(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetPlayerResponse> 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<GetPlayerResponse>(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> PostPlayer(PostPlayer request)
|
|
||||||
{
|
|
||||||
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
|
|
||||||
return await client.PostAsync("Player", content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string> CreateGuestUser();
|
|
||||||
Task<bool> 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<string> 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<bool> IsPlayer1(string sessionName, string playerName)
|
|
||||||
{
|
|
||||||
var session = await repository.GetGame(sessionName);
|
|
||||||
return session?.Session.Player1 == playerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<HttpResponseMessage> DeleteAsync(string requestUri);
|
|
||||||
Task<HttpResponseMessage> GetAsync(string requestUri);
|
|
||||||
Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
|
|
||||||
Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AuthenticatedHttpClient : HttpClient, IAuthenticatedHttpClient
|
|
||||||
{
|
|
||||||
private readonly ILogger<AuthenticatedHttpClient> logger;
|
|
||||||
private readonly string identityServerUrl;
|
|
||||||
private TokenResponse tokenResponse;
|
|
||||||
private readonly string clientId;
|
|
||||||
private readonly string clientSecret;
|
|
||||||
|
|
||||||
public AuthenticatedHttpClient(ILogger<AuthenticatedHttpClient> logger, IConfiguration configuration) : base()
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
identityServerUrl = configuration["AppSettings:IdentityServer"];
|
|
||||||
clientId = configuration["AppSettings:ClientId"];
|
|
||||||
clientSecret = configuration["AppSettings:ClientSecret"];
|
|
||||||
BaseAddress = new Uri(configuration["AppSettings:GameboardShogiApi"]);
|
|
||||||
}
|
|
||||||
private async Task RefreshBearerToken()
|
|
||||||
{
|
|
||||||
var disco = await this.GetDiscoveryDocumentAsync(identityServerUrl);
|
|
||||||
if (disco.IsError)
|
|
||||||
{
|
|
||||||
logger.LogError("{DiscoveryErrorType}", disco.ErrorType);
|
|
||||||
throw new Exception(disco.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = new ClientCredentialsTokenRequest
|
|
||||||
{
|
|
||||||
Address = disco.TokenEndpoint,
|
|
||||||
ClientId = clientId,
|
|
||||||
ClientSecret = clientSecret
|
|
||||||
};
|
|
||||||
var response = await this.RequestClientCredentialsTokenAsync(request);
|
|
||||||
if (response.IsError)
|
|
||||||
{
|
|
||||||
throw new Exception(response.Error);
|
|
||||||
}
|
|
||||||
tokenResponse = response;
|
|
||||||
logger.LogInformation("Refreshing Bearer Token to {BaseAddress}", BaseAddress);
|
|
||||||
this.SetBearerToken(tokenResponse.AccessToken);
|
|
||||||
}
|
|
||||||
public async new Task<HttpResponseMessage> 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<HttpResponseMessage> GetAsync(string requestUri)
|
|
||||||
{
|
|
||||||
var response = await base.GetAsync(requestUri);
|
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
await RefreshBearerToken();
|
|
||||||
response = await base.GetAsync(requestUri);
|
|
||||||
}
|
|
||||||
logger.LogInformation(
|
|
||||||
"Repository GET to {BaseUrl}{RequestUrl} \nResponse: {Response}\n",
|
|
||||||
BaseAddress,
|
|
||||||
requestUri,
|
|
||||||
await response.Content.ReadAsStringAsync());
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
public async new Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
|
|
||||||
{
|
|
||||||
var response = await base.PostAsync(requestUri, content);
|
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
await RefreshBearerToken();
|
|
||||||
response = await base.PostAsync(requestUri, content);
|
|
||||||
}
|
|
||||||
logger.LogInformation(
|
|
||||||
"Repository POST to {BaseUrl}{RequestUrl} \n\tRespCode: {RespCode} \n\tRequest: {Request}\n\tResponse: {Response}\n",
|
|
||||||
BaseAddress,
|
|
||||||
requestUri,
|
|
||||||
response.StatusCode,
|
|
||||||
await content.ReadAsStringAsync(),
|
|
||||||
await response.Content.ReadAsStringAsync());
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
public async new Task<HttpResponseMessage> DeleteAsync(string requestUri)
|
|
||||||
{
|
|
||||||
var response = await base.DeleteAsync(requestUri);
|
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
await RefreshBearerToken();
|
|
||||||
response = await base.DeleteAsync(requestUri);
|
|
||||||
}
|
|
||||||
logger.LogInformation("Repository DELETE to {BaseUrl}{RequestUrl}", BaseAddress, requestUri);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<JoinByCodeRequest>
|
||||||
|
{
|
||||||
|
public JoinByCodeRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(_ => _.Action).Equal(ClientAction.JoinByCode);
|
||||||
|
RuleFor(_ => _.JoinCode).NotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<JoinGameRequest>
|
||||||
|
{
|
||||||
|
public JoinGameRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(_ => _.Action).Equal(ClientAction.JoinGame);
|
||||||
|
RuleFor(_ => _.GameName).NotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
Gameboard.ShogiUI.Sockets/Services/SocketService.cs
Normal file
132
Gameboard.ShogiUI.Sockets/Services/SocketService.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Services a single websocket connection. Authenticates the socket connection, accepts messages, and sends messages.
|
||||||
|
/// </summary>
|
||||||
|
public class SocketService : ISocketService
|
||||||
|
{
|
||||||
|
private readonly ILogger<SocketService> logger;
|
||||||
|
private readonly ISocketConnectionManager communicationManager;
|
||||||
|
private readonly IGameboardRepository gameboardRepository;
|
||||||
|
private readonly IGameboardManager gameboardManager;
|
||||||
|
private readonly ISocketTokenCache tokenManager;
|
||||||
|
private readonly IJoinByCodeHandler joinByCodeHandler;
|
||||||
|
private readonly IValidator<JoinByCodeRequest> joinByCodeRequestValidator;
|
||||||
|
private readonly IValidator<JoinGameRequest> joinGameRequestValidator;
|
||||||
|
|
||||||
|
public SocketService(
|
||||||
|
ILogger<SocketService> logger,
|
||||||
|
ISocketConnectionManager communicationManager,
|
||||||
|
IGameboardRepository gameboardRepository,
|
||||||
|
IGameboardManager gameboardManager,
|
||||||
|
ISocketTokenCache tokenManager,
|
||||||
|
IJoinByCodeHandler joinByCodeHandler,
|
||||||
|
IValidator<JoinByCodeRequest> joinByCodeRequestValidator,
|
||||||
|
IValidator<JoinGameRequest> 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<Request>(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<JoinByCodeRequest>(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<bool> ValidateRequestAndReplyIfInvalid<TRequest>(WebSocket socket, IValidator<TRequest> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
public class JsonRequest
|
||||||
{
|
{
|
||||||
10
Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs
Normal file
10
Gameboard.ShogiUI.Sockets/Services/Utility/Request.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs
Normal file
9
Gameboard.ShogiUI.Sockets/Services/Utility/Response.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
Normal file
43
Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Standardizes the claims from third party issuers. Also registers new msal users in the database.
|
||||||
|
/// </summary>
|
||||||
|
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<ClaimsPrincipal> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,149 +1,217 @@
|
|||||||
using Gameboard.ShogiUI.Sockets.Repositories.RepositoryManagers;
|
using FluentValidation;
|
||||||
using Gameboard.ShogiUI.Sockets.Extensions;
|
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.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Http;
|
||||||
|
using Microsoft.Identity.Client;
|
||||||
|
using Microsoft.Identity.Web;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Gameboard.ShogiUI.Sockets.Managers;
|
using System.Security.Claims;
|
||||||
using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
|
using System.Text;
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories;
|
using System.Threading.Tasks;
|
||||||
using Gameboard.ShogiUI.Sockets.Repositories.Utility;
|
|
||||||
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types;
|
|
||||||
|
|
||||||
namespace Gameboard.ShogiUI.Sockets
|
namespace Gameboard.ShogiUI.Sockets
|
||||||
{
|
{
|
||||||
public class Startup
|
public class Startup
|
||||||
{
|
{
|
||||||
public Startup(IConfiguration configuration)
|
public Startup(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
Configuration = 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.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
// Socket ActionHandlers
|
services.AddSingleton<IJoinByCodeHandler, JoinByCodeHandler>();
|
||||||
services.AddSingleton<CreateGameHandler>();
|
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
||||||
services.AddSingleton<JoinByCodeHandler>();
|
services.AddSingleton<ISocketTokenCache, SocketTokenCache>();
|
||||||
services.AddSingleton<JoinGameHandler>();
|
services.AddSingleton<IGameboardManager, GameboardManager>();
|
||||||
services.AddSingleton<ListGamesHandler>();
|
services.AddSingleton<IValidator<JoinByCodeRequest>, JoinByCodeRequestValidator>();
|
||||||
services.AddSingleton<LoadGameHandler>();
|
services.AddSingleton<IValidator<JoinGameRequest>, JoinGameRequestValidator>();
|
||||||
services.AddSingleton<MoveHandler>();
|
services.AddSingleton<ISocketService, SocketService>();
|
||||||
|
services.AddTransient<IGameboardRepository, GameboardRepository>();
|
||||||
|
services.AddSingleton<IClaimsTransformation, ShogiUserClaimsTransformer>();
|
||||||
|
services.AddHttpClient("couchdb", c =>
|
||||||
|
{
|
||||||
|
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin"));
|
||||||
|
c.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||||
|
c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}");
|
||||||
|
|
||||||
// Managers
|
var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/";
|
||||||
services.AddSingleton<ISocketCommunicationManager, SocketCommunicationManager>();
|
c.BaseAddress = new Uri(baseUrl);
|
||||||
services.AddSingleton<ISocketTokenManager, SocketTokenManager>();
|
});
|
||||||
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
|
||||||
services.AddScoped<IGameboardRepositoryManager, GameboardRepositoryManager>();
|
|
||||||
services.AddSingleton<ActionHandlerResolver>(sp => action =>
|
|
||||||
{
|
|
||||||
return action switch
|
|
||||||
{
|
|
||||||
ClientAction.ListGames => sp.GetService<ListGamesHandler>(),
|
|
||||||
ClientAction.CreateGame => sp.GetService<CreateGameHandler>(),
|
|
||||||
ClientAction.JoinGame => sp.GetService<JoinGameHandler>(),
|
|
||||||
ClientAction.JoinByCode => sp.GetService<JoinByCodeHandler>(),
|
|
||||||
ClientAction.LoadGame => sp.GetService<LoadGameHandler>(),
|
|
||||||
ClientAction.Move => sp.GetService<MoveHandler>(),
|
|
||||||
_ => throw new KeyNotFoundException($"Unable to resolve {nameof(IActionHandler)} for {nameof(ClientAction)} {action}"),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Repositories
|
services
|
||||||
services.AddTransient<IGameboardRepository, GameboardRepository>();
|
.AddControllers()
|
||||||
services.AddSingleton<IAuthenticatedHttpClient, AuthenticatedHttpClient>();
|
.AddNewtonsoftJson(options =>
|
||||||
|
{
|
||||||
|
options.SerializerSettings.Formatting = Formatting.Indented;
|
||||||
|
options.SerializerSettings.ContractResolver = new DefaultContractResolver
|
||||||
|
{
|
||||||
|
NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true }
|
||||||
|
};
|
||||||
|
options.SerializerSettings.Converters = new[] { new StringEnumConverter() };
|
||||||
|
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||||
|
});
|
||||||
|
|
||||||
services.AddControllers();
|
services.AddAuthentication("CookieOrJwt")
|
||||||
|
.AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options =>
|
||||||
|
{
|
||||||
|
options.ForwardDefaultSelector = context =>
|
||||||
|
{
|
||||||
|
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
|
||||||
|
return bearerAuth
|
||||||
|
? JwtBearerDefaults.AuthenticationScheme
|
||||||
|
: CookieAuthenticationDefaults.AuthenticationScheme;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.AddCookie(options =>
|
||||||
|
{
|
||||||
|
options.Cookie.Name = "session-id";
|
||||||
|
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
|
||||||
|
options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
|
||||||
|
options.SlidingExpiration = true;
|
||||||
|
})
|
||||||
|
.AddMicrosoftIdentityWebApi(Configuration);
|
||||||
|
|
||||||
services
|
services.AddSwaggerDocument(config =>
|
||||||
.AddAuthentication(options =>
|
{
|
||||||
{
|
//config.AddSecurity("bearer", Enumerable.Empty<string>(), new NSwag.OpenApiSecurityScheme
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
//{
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
// Type = NSwag.OpenApiSecuritySchemeType.OAuth2,
|
||||||
})
|
// Flow = NSwag.OpenApiOAuth2Flow.Implicit,
|
||||||
.AddJwtBearer(options =>
|
// Flows = new NSwag.OpenApiOAuthFlows
|
||||||
{
|
// {
|
||||||
options.Authority = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0";
|
// Implicit = new NSwag.OpenApiOAuthFlow
|
||||||
options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122";
|
// {
|
||||||
options.TokenValidationParameters.ValidateIssuer = true;
|
// Scopes =
|
||||||
options.TokenValidationParameters.ValidateAudience = true;
|
// }
|
||||||
});
|
// }
|
||||||
}
|
//});
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This just ensures anyone with a microsoft account can make API calls.
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketConnectionManager socketConnectionManager)
|
config.AddSecurity("bearer", new NSwag.OpenApiSecurityScheme
|
||||||
{
|
{
|
||||||
var origins = new[] {
|
Type = NSwag.OpenApiSecuritySchemeType.OAuth2,
|
||||||
"http://localhost:3000", "https://localhost:3000",
|
Flow = NSwag.OpenApiOAuth2Flow.Implicit,
|
||||||
"http://127.0.0.1:3000", "https://127.0.0.1:3000",
|
AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||||
"https://dev.lucaserver.space", "https://lucaserver.space"
|
TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||||
};
|
Scopes = new Dictionary<string, string> {
|
||||||
var socketOptions = new WebSocketOptions();
|
{ "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" },
|
||||||
foreach (var o in origins)
|
{ "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/ShogiAdmin", "Admin scope" }
|
||||||
socketOptions.AllowedOrigins.Add(o);
|
},
|
||||||
|
Scheme = "bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
In = NSwag.OpenApiSecurityApiKeyLocation.Header,
|
||||||
|
});
|
||||||
|
config.PostProcess = document =>
|
||||||
|
{
|
||||||
|
document.Info.Title = "Gameboard.ShogiUI.Sockets";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
// Remove default HttpClient logging.
|
||||||
{
|
services.RemoveAll<IHttpMessageHandlerBuilderFilter>();
|
||||||
app.UseDeveloperExceptionPage();
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
app.UseHsts();
|
|
||||||
}
|
|
||||||
app
|
|
||||||
.UseRequestResponseLogging()
|
|
||||||
.UseCors(
|
|
||||||
opt => opt
|
|
||||||
.WithOrigins(origins)
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowCredentials()
|
|
||||||
)
|
|
||||||
.UseRouting()
|
|
||||||
.UseAuthentication()
|
|
||||||
.UseAuthorization()
|
|
||||||
.UseWebSockets(socketOptions)
|
|
||||||
.UseEndpoints(endpoints =>
|
|
||||||
{
|
|
||||||
endpoints.MapControllers();
|
|
||||||
})
|
|
||||||
.Use(async (context, next) =>
|
|
||||||
{
|
|
||||||
var isUpgradeHeader = context
|
|
||||||
.Request
|
|
||||||
.Headers
|
|
||||||
.Any(h => h.Key.Contains("upgrade", StringComparison.InvariantCultureIgnoreCase)
|
|
||||||
&& h.Value.ToString().Contains("websocket", StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
if (isUpgradeHeader)
|
|
||||||
{
|
|
||||||
await socketConnectionManager.HandleSocketRequest(context);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
{
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager)
|
||||||
Formatting = Formatting.Indented,
|
{
|
||||||
ContractResolver = new DefaultContractResolver
|
var origins = new[] {
|
||||||
{
|
"http://localhost:3000", "https://localhost:3000",
|
||||||
NamingStrategy = new CamelCaseNamingStrategy(),
|
"http://127.0.0.1:3000", "https://127.0.0.1:3000",
|
||||||
},
|
"https://dev.lucaserver.space", "https://lucaserver.space"
|
||||||
Converters = new[] { new StringEnumConverter() },
|
};
|
||||||
NullValueHandling = NullValueHandling.Ignore
|
var socketOptions = new WebSocketOptions();
|
||||||
};
|
foreach (var o in origins)
|
||||||
}
|
socketOptions.AllowedOrigins.Add(o);
|
||||||
}
|
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
var client = PublicClientApplicationBuilder
|
||||||
|
.Create(Configuration["AzureAd:ClientId"])
|
||||||
|
.WithLogging(
|
||||||
|
(level, message, pii) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine(message);
|
||||||
|
},
|
||||||
|
LogLevel.Verbose,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
app
|
||||||
|
.UseRequestResponseLogging()
|
||||||
|
.UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials())
|
||||||
|
.UseRouting()
|
||||||
|
.UseAuthentication()
|
||||||
|
.UseAuthorization()
|
||||||
|
.UseOpenApi()
|
||||||
|
.UseSwaggerUi3(config =>
|
||||||
|
{
|
||||||
|
config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings()
|
||||||
|
{
|
||||||
|
ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff",
|
||||||
|
UsePkceWithAuthorizationCodeGrant = true
|
||||||
|
};
|
||||||
|
//config.WithCredentials = true;
|
||||||
|
})
|
||||||
|
.UseWebSockets(socketOptions)
|
||||||
|
.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapControllers();
|
||||||
|
})
|
||||||
|
.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
if (context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
await socketConnectionManager.HandleSocketRequest(context);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
ContractResolver = new DefaultContractResolver
|
||||||
|
{
|
||||||
|
NamingStrategy = new CamelCaseNamingStrategy
|
||||||
|
{
|
||||||
|
ProcessDictionaryKeys = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Converters = new[] { new StringEnumConverter() },
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Gameboard.ShogiUI.Sockets.Models;
|
||||||
|
using PathFinding;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.Utilities
|
||||||
|
{
|
||||||
|
public class CoordsToNotationCollection : Dictionary<string, Piece?>, IPlanarCollection<Piece>
|
||||||
|
{
|
||||||
|
public delegate void ForEachDelegate(Piece element, Vector2 position);
|
||||||
|
|
||||||
|
public CoordsToNotationCollection() : base(81, StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CoordsToNotationCollection(Dictionary<string, Piece?> board) : base(board, StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Piece? this[Vector2 vector]
|
||||||
|
{
|
||||||
|
get => this[NotationHelper.ToBoardNotation(vector)];
|
||||||
|
set => this[NotationHelper.ToBoardNotation(vector)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Piece? this[int x, int y]
|
||||||
|
{
|
||||||
|
get => this[NotationHelper.ToBoardNotation(x, y)];
|
||||||
|
set => this[NotationHelper.ToBoardNotation(x, y)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ForEachNotNull(ForEachDelegate callback)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < 9; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < 9; y++)
|
||||||
|
{
|
||||||
|
var position = new Vector2(x, y);
|
||||||
|
var elem = this[position];
|
||||||
|
if (elem != null)
|
||||||
|
callback(elem, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs
Normal file
36
Gameboard.ShogiUI.Sockets/Utilities/NotationHelper.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.Sockets.Utilities
|
||||||
|
{
|
||||||
|
public static class NotationHelper
|
||||||
|
{
|
||||||
|
private static readonly string BoardNotationRegex = @"(?<file>[a-iA-I])(?<rank>[1-9])";
|
||||||
|
private static readonly char A = 'A';
|
||||||
|
|
||||||
|
public static string ToBoardNotation(Vector2 vector)
|
||||||
|
{
|
||||||
|
return ToBoardNotation((int)vector.X, (int)vector.Y);
|
||||||
|
}
|
||||||
|
public static string ToBoardNotation(int x, int y)
|
||||||
|
{
|
||||||
|
var file = (char)(x + A);
|
||||||
|
var rank = y + 1;
|
||||||
|
return $"{file}{rank}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector2 FromBoardNotation(string notation)
|
||||||
|
{
|
||||||
|
notation = notation.ToUpper();
|
||||||
|
if (Regex.IsMatch(notation, BoardNotationRegex))
|
||||||
|
{
|
||||||
|
var match = Regex.Match(notation, BoardNotationRegex);
|
||||||
|
char file = match.Groups["file"].Value[0];
|
||||||
|
int rank = int.Parse(match.Groups["rank"].Value);
|
||||||
|
return new Vector2(file - A, rank - 1);
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
{
|
{
|
||||||
"AppSettings": {
|
"AppSettings": {
|
||||||
"IdentityServer": "https://identity.lucaserver.space/",
|
"CouchDB": {
|
||||||
"GameboardShogiApi": "https://dev.lucaserver.space/Gameboard.Shogi.Api/",
|
"Database": "shogi-dev",
|
||||||
"ClientId": "DevClientId",
|
"Url": "http://192.168.1.15:5984"
|
||||||
"ClientSecret": "DevSecret",
|
}
|
||||||
"Scope": "DevEnvironment"
|
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Warning",
|
||||||
"Microsoft": "Warning",
|
"Microsoft": "Warning",
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"AzureAd": {
|
||||||
|
"Instance": "https://login.microsoftonline.com/",
|
||||||
|
"ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff",
|
||||||
|
"TenantId": "common",
|
||||||
|
"Audience": "c1e94676-cab0-42ba-8b6c-9532b8486fff",
|
||||||
|
"ClientSecret": ""
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AutoFixture" Version="4.17.0" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="6.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02" />
|
||||||
|
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||||
|
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.2-pre.12" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
|
||||||
|
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using PathFinding;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.UnitTests.PathFinding
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class PathFinder2DShould
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void Maths()
|
||||||
|
{
|
||||||
|
var result = PathFinder2D<IPlanarElement>.IsPathable(
|
||||||
|
new Vector2(2, 2),
|
||||||
|
new Vector2(7, 7),
|
||||||
|
new Vector2(1, 1)
|
||||||
|
);
|
||||||
|
result.Should().BeTrue();
|
||||||
|
|
||||||
|
result = PathFinder2D<IPlanarElement>.IsPathable(
|
||||||
|
new Vector2(2, 2),
|
||||||
|
new Vector2(7, 7),
|
||||||
|
new Vector2(0, 0)
|
||||||
|
);
|
||||||
|
result.Should().BeFalse();
|
||||||
|
|
||||||
|
result = PathFinder2D<IPlanarElement>.IsPathable(
|
||||||
|
new Vector2(2, 2),
|
||||||
|
new Vector2(7, 7),
|
||||||
|
new Vector2(-1, 1)
|
||||||
|
);
|
||||||
|
result.Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using AutoFixture;
|
||||||
|
using FluentAssertions;
|
||||||
|
using FluentAssertions.Execution;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.UnitTests.PathFinding
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class PlanarCollectionShould
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void Index()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var collection = new TestPlanarCollection();
|
||||||
|
var expected1 = new SimpleElement(1);
|
||||||
|
var expected2 = new SimpleElement(2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
collection[0, 0] = expected1;
|
||||||
|
collection[2, 1] = expected2;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
collection[0, 0].Should().Be(expected1);
|
||||||
|
collection[2, 1].Should().Be(expected2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Iterate()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var planarCollection = new TestPlanarCollection();
|
||||||
|
planarCollection[0, 0] = new SimpleElement(1);
|
||||||
|
planarCollection[0, 1] = new SimpleElement(2);
|
||||||
|
planarCollection[0, 2] = new SimpleElement(3);
|
||||||
|
planarCollection[1, 0] = new SimpleElement(4);
|
||||||
|
planarCollection[1, 1] = new SimpleElement(5);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = new List<SimpleElement>();
|
||||||
|
foreach (var elem in planarCollection)
|
||||||
|
actual.Add(elem);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
using (new AssertionScope())
|
||||||
|
{
|
||||||
|
actual[0].Number.Should().Be(1);
|
||||||
|
actual[1].Number.Should().Be(2);
|
||||||
|
actual[2].Number.Should().Be(3);
|
||||||
|
actual[3].Number.Should().Be(4);
|
||||||
|
actual[4].Number.Should().Be(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using PathFinding;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Gameboard.ShogiUI.UnitTests.PathFinding
|
||||||
|
{
|
||||||
|
public class SimpleElement : IPlanarElement
|
||||||
|
{
|
||||||
|
public int Number { get; }
|
||||||
|
public MoveSet MoveSet => null;
|
||||||
|
public bool IsUpsideDown => false;
|
||||||
|
|
||||||
|
public SimpleElement(int number)
|
||||||
|
{
|
||||||
|
Number = number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestPlanarCollection : IPlanarCollection<SimpleElement>
|
||||||
|
{
|
||||||
|
private readonly SimpleElement[,] array;
|
||||||
|
public TestPlanarCollection()
|
||||||
|
{
|
||||||
|
array = new SimpleElement[3, 3];
|
||||||
|
}
|
||||||
|
public SimpleElement this[int x, int y]
|
||||||
|
{
|
||||||
|
get => array[x, y];
|
||||||
|
set => array[x, y] = value;
|
||||||
|
}
|
||||||
|
public SimpleElement this[Vector2 vector]
|
||||||
|
{
|
||||||
|
get => this[(int)vector.X, (int)vector.Y];
|
||||||
|
set => this[(int)vector.X, (int)vector.Y] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<SimpleElement> GetEnumerator()
|
||||||
|
{
|
||||||
|
foreach (var e in array)
|
||||||
|
yield return e;
|
||||||
|
}
|
||||||
|
//IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
//{
|
||||||
|
// return array.GetEnumerator();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs
Normal file
15
Gameboard.ShogiUI.UnitTests/Rules/ShogiBoardShould.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Gameboard.ShogiUI.Sockets.Models;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using WhichPerspective = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPerspective;
|
||||||
|
using WhichPiece = Gameboard.ShogiUI.Sockets.ServiceModels.Types.WhichPiece;
|
||||||
|
namespace Gameboard.ShogiUI.UnitTests.Rules
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class ShogiBoardShould
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user