From 93027e8c577106e75aee1149dfc2d878aa25722f Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Sun, 30 Oct 2022 12:03:16 -0500 Subject: [PATCH] squash a bunch of commits --- .gitignore | 2 + .../Api/GetGuestToken.cs | 18 - .../Api/GetSessions.cs | 11 - .../Api/GetToken.cs | 14 - .../Api/PostGameInvitation.cs | 20 - .../Api/PostMove.cs | 11 - .../Api/PostSession.cs | 8 - .../Socket/CreateGame.cs | 20 - .../Socket/IRequest.cs | 9 - .../Socket/IResponse.cs | 7 - .../Socket/JoinGame.cs | 41 -- .../Socket/Move.cs | 19 - .../Types/ClientActionEnum.cs | 10 - .../Types/SessionMetadata.cs | 12 - .../Types/WhichPerspective.cs | 8 - Gameboard.ShogiUI.Sockets.sln | 55 -- .../Controllers/GameController.cs | 253 -------- .../Controllers/SocketController.cs | 102 ---- .../Extensions/Extensions.cs | 29 - .../ClientActionHandlers/JoinByCodeHandler.cs | 64 --- .../Managers/GameboardManager.cs | 74 --- .../Managers/SocketConnectionManager.cs | 98 ---- Gameboard.ShogiUI.Sockets/Models/User.cs | 83 --- .../Repositories/GameboardRepository.cs | 310 ---------- .../JoinByCodeRequestValidator.cs | 15 - .../JoinGameRequestValidator.cs | 15 - .../ShogiUserClaimsTransformer.cs | 44 -- Shogi.AcceptanceTests/AcceptanceTests.cs | 26 - .../Api/Commands/CreateGuestTokenResponse.cs | 17 + .../Api/Commands/CreateSessionCommand.cs | 10 + .../Api/Commands/CreateTokenResponse.cs | 13 + .../Api/Commands/MovePieceCommand.cs | 10 + .../Api/Queries/ReadAllSessionsResponse.cs | 10 + .../Api/Queries/ReadSessionResponse.cs | 8 + .../Shogi.Contracts.csproj | 2 +- Shogi.Contracts/Socket/CreateGame.cs | 13 + Shogi.Contracts/Socket/ISocketRequest.cs | 9 + Shogi.Contracts/Socket/ISocketResponse.cs | 13 + Shogi.Contracts/Socket/Move.cs | 18 + .../Types/BoardState.cs | 2 +- .../Types/Move.cs | 2 +- .../Types/Piece.cs | 2 +- .../Types/Session.cs | 4 +- Shogi.Contracts/Types/SessionMetadata.cs | 8 + Shogi.Contracts/Types/SocketAction.cs | 9 + .../Types/User.cs | 2 +- Shogi.Contracts/Types/WhichPerspective.cs | 8 + .../Types/WhichPiece.cs | 2 +- .../Post Deployment/Script.PostDeployment.sql | 13 + .../Scripts/PopulateLoginPlatforms.sql | 16 + Shogi.Database/Session/Session.sql | 1 + .../Stored Procedures/CreateBoardState.sql | 16 + .../Stored Procedures/CreateSession.sql | 24 + .../ReadAllSessionsMetadata.sql | 12 + .../Stored Procedures/UpdateSession.sql | 14 + Shogi.Database/Session/Tables/BoardState.sql | 10 + Shogi.Database/Session/Tables/Session.sql | 16 + Shogi.Database/Session/Types/JsonDocument.sql | 2 + Shogi.Database/Session/Types/SessionName.sql | 2 + Shogi.Database/Shogi.Database.sqlproj | 91 +++ .../User/StoredProcedures/CreateUser.sql | 14 + .../User/StoredProcedures/ReadUser.sql | 11 + Shogi.Database/User/Tables/LoginPlatform.sql | 4 + Shogi.Database/User/Tables/User.sql | 11 + Shogi.Database/User/Types/UserName.sql | 2 + Shogi.Database/User/User.sql | 1 + Shogi.Domain.UnitTests/RookShould.cs | 227 -------- .../Shogi.Domain.UnitTests.csproj | 28 - Shogi.Domain.UnitTests/ShogiShould.cs | 463 --------------- Shogi.Domain/Aggregates/Session.cs | 242 ++++++++ Shogi.Domain/BoardState.cs | 437 +++++++------- Shogi.Domain/DomainExtensions.cs | 4 +- Shogi.Domain/Pathing/Path.cs | 7 +- Shogi.Domain/Pieces/Bishop.cs | 47 -- Shogi.Domain/Pieces/GoldGeneral.cs | 31 - Shogi.Domain/Pieces/King.cs | 27 - Shogi.Domain/Pieces/Knight.cs | 32 -- Shogi.Domain/Pieces/Lance.cs | 31 - Shogi.Domain/Pieces/Pawn.cs | 31 - Shogi.Domain/Pieces/Piece.cs | 95 --- Shogi.Domain/Pieces/Rook.cs | 52 -- Shogi.Domain/Pieces/SilverGeneral.cs | 35 -- Shogi.Domain/Session.cs | 257 --------- Shogi.Domain/SessionMetadata.cs | 28 - Shogi.Domain/Shogi.Domain.csproj | 12 +- Shogi.Domain/StandardRules.cs | 6 +- Shogi.Domain/ValueObjects/Bishop.cs | 47 ++ Shogi.Domain/ValueObjects/GoldGeneral.cs | 31 + Shogi.Domain/ValueObjects/King.cs | 27 + Shogi.Domain/ValueObjects/Knight.cs | 32 ++ Shogi.Domain/ValueObjects/Lance.cs | 31 + Shogi.Domain/ValueObjects/Pawn.cs | 31 + Shogi.Domain/ValueObjects/Piece.cs | 95 +++ Shogi.Domain/ValueObjects/Rook.cs | 51 ++ Shogi.Domain/ValueObjects/SilverGeneral.cs | 35 ++ .../.config/dotnet-tools.json | 0 .../Controllers/SessionController.cs | 217 +++++++ Shogi.Sockets/Controllers/UserController.cs | 108 ++++ .../ExampleAnonymousSessionMiddleware.cs | 6 +- Shogi.Sockets/Extensions/Extensions.cs | 30 + .../Extensions/LogMiddleware.cs | 2 +- .../Extensions/WebSocketExtensions.cs | 7 +- .../Managers/ModelMapper.cs | 31 +- .../Managers/SocketConnectionManager.cs | 89 +++ .../Managers/SocketTokenCache.cs | 2 +- Shogi.Sockets/Models/User.cs | 42 ++ .../Models/WhichLoginPlatform.cs | 2 +- .../Program.cs | 159 +++-- .../PublishProfiles/FolderProfile.pubxml | 0 .../local/secrets1.arm.json | 0 .../Properties/launchSettings.json | 0 .../Properties/serviceDependencies.json | 0 .../Properties/serviceDependencies.local.json | 0 .../Readme.md | 2 +- .../CouchModels/BoardStateDocument.cs | 15 +- .../CouchModels/CouchCreatedResult.cs | 2 +- .../Repositories/CouchModels/CouchDocument.cs | 2 +- .../CouchModels/CouchFindResult.cs | 2 +- .../CouchModels/CouchViewResult.cs | 2 +- .../Repositories/CouchModels/Move.cs | 2 +- .../Repositories/CouchModels/Piece.cs | 2 +- .../CouchModels/SessionDocument.cs | 9 +- .../Repositories/CouchModels/UserDocument.cs | 4 +- .../CouchModels/WhichDocumentType.cs | 2 +- .../Repositories/GameboardRepository.cs | 232 ++++++++ Shogi.Sockets/Repositories/QueryRepository.cs | 28 + .../Repositories/SessionRepository.cs | 37 ++ Shogi.Sockets/Repositories/UserRepository.cs | 47 ++ .../Services/SocketService.cs | 51 +- .../Shogi.Api.csproj | 26 +- Shogi.Sockets/ShogiUserClaimsTransformer.cs | 89 +++ .../appsettings.Development.json | 0 .../appsettings.json | 12 +- Shogi.UI/.config/dotnet-tools.json | 12 + Shogi.UI/App.razor | 31 + Shogi.UI/Pages/Authentication.razor | 7 + Shogi.UI/Pages/Home/Account/AccountManager.cs | 138 +++++ Shogi.UI/Pages/Home/Account/AccountState.cs | 28 + .../Home/Account/LocalStorageExtensions.cs | 24 + Shogi.UI/Pages/Home/Account/LoginEventArgs.cs | 7 + Shogi.UI/Pages/Home/Account/User.cs | 12 + .../Home/Account/WhichAccountPlatform.cs | 8 + .../Pages/Home/Api/CookieMessageHandler.cs | 18 + Shogi.UI/Pages/Home/Api/IShogiApi.cs | 17 + Shogi.UI/Pages/Home/Api/MsalMessageHandler.cs | 21 + Shogi.UI/Pages/Home/Api/ShogiApi.cs | 98 ++++ Shogi.UI/Pages/Home/GameBoard.razor | 62 ++ Shogi.UI/Pages/Home/GameBoard.razor.css | 63 ++ Shogi.UI/Pages/Home/GameBrowser.razor | 109 ++++ Shogi.UI/Pages/Home/GameBrowser.razor.css | 13 + Shogi.UI/Pages/Home/GamePiece.razor | 44 ++ Shogi.UI/Pages/Home/GamePiece.razor.css | 8 + Shogi.UI/Pages/Home/Home.razor | 46 ++ Shogi.UI/Pages/Home/Home.razor.css | 30 + Shogi.UI/Pages/Home/LoginModal.razor | 56 ++ Shogi.UI/Pages/Home/LoginModal.razor.css | 21 + Shogi.UI/Pages/Home/PageHeader.razor | 31 + Shogi.UI/Pages/Home/PageHeader.razor.css | 20 + Shogi.UI/Pages/Home/Pieces/Bishop.razor | 32 ++ .../Pages/Home/Pieces/ChallengingKing.razor | 10 + Shogi.UI/Pages/Home/Pieces/GoldGeneral.razor | 10 + Shogi.UI/Pages/Home/Pieces/Knight.razor | 31 + Shogi.UI/Pages/Home/Pieces/Lance.razor | 31 + Shogi.UI/Pages/Home/Pieces/Pawn.razor | 31 + Shogi.UI/Pages/Home/Pieces/ReigningKing.razor | 10 + Shogi.UI/Pages/Home/Pieces/Rook.razor | 31 + .../Pages/Home/Pieces/SilverGeneral.razor | 31 + Shogi.UI/Program.cs | 65 +++ Shogi.UI/Properties/Resources.Designer.cs | 63 ++ Shogi.UI/Properties/Resources.resx | 101 ++++ Shogi.UI/Properties/launchSettings.json | 15 + Shogi.UI/Properties/serviceDependencies.json | 8 + .../Properties/serviceDependencies.local.json | 8 + Shogi.UI/Shared/LocalStorage.cs | 51 ++ Shogi.UI/Shared/LoginDisplay.razor | 24 + Shogi.UI/Shared/MainLayout.razor | 4 + Shogi.UI/Shared/MainLayout.razor.css | 3 + Shogi.UI/Shared/Modal/ModalService.cs | 54 ++ .../Modal/ModalVisibilityChangedEventArgs.cs | 9 + Shogi.UI/Shared/Modal/Modals.razor | 39 ++ Shogi.UI/Shared/Modal/Modals.razor.css | 21 + Shogi.UI/Shared/MyNotAuthorized.razor | 5 + Shogi.UI/Shared/RedirectToLogin.razor | 14 + Shogi.UI/Shared/RotatingCogsSvg.razor | 28 + Shogi.UI/Shared/RotatingCogsSvg.razor.css | 15 + Shogi.UI/Shared/ShogiSocket.cs | 89 +++ Shogi.UI/Shogi.UI.csproj | 48 ++ Shogi.UI/_Imports.razor | 15 + Shogi.UI/wwwroot/appsettings.Development.json | 6 + Shogi.UI/wwwroot/appsettings.json | 19 + Shogi.UI/wwwroot/css/app.css | 131 +++++ .../wwwroot/css/bootstrap/bootstrap.min.css | 7 + .../css/bootstrap/bootstrap.min.css.map | 1 + .../wwwroot/css/bootstrap/bootstrap.min.js | 7 + .../css/bootstrap/bootstrap.min.js.map | 1 + Shogi.UI/wwwroot/css/open-iconic/FONT-LICENSE | 86 +++ Shogi.UI/wwwroot/css/open-iconic/ICON-LICENSE | 21 + Shogi.UI/wwwroot/css/open-iconic/README.md | 114 ++++ .../font/css/open-iconic-bootstrap.min.css | 1 + .../open-iconic/font/fonts/open-iconic.eot | Bin 0 -> 28196 bytes .../open-iconic/font/fonts/open-iconic.otf | Bin 0 -> 20996 bytes .../open-iconic/font/fonts/open-iconic.svg | 543 ++++++++++++++++++ .../open-iconic/font/fonts/open-iconic.ttf | Bin 0 -> 28028 bytes .../open-iconic/font/fonts/open-iconic.woff | Bin 0 -> 14984 bytes Shogi.UI/wwwroot/favicon.ico | Bin 0 -> 5430 bytes Shogi.UI/wwwroot/icon-192.png | Bin 0 -> 2626 bytes Shogi.UI/wwwroot/index.html | 35 ++ Shogi.UI/wwwroot/sample-data/weather.json | 27 + Shogi.sln | 77 +++ Tests/AcceptanceTests/AcceptanceTests.cs | 25 + .../AcceptanceTests/AcceptanceTests.csproj | 6 +- .../TestSetup/MsalTestFixture - Copy.cs | 28 +- .../TestSetup/MsalTestFixture.cs | 100 ++++ .../AcceptanceTests}/Usings.cs | 0 .../AcceptanceTests}/appsettings.json | 0 .../UnitTests}/NotationShould.cs | 4 +- Tests/UnitTests/RookShould.cs | 225 ++++++++ .../ServiceModelsShouldSerialize.cs | 25 + .../UnitTests}/ShogiBoardStateShould.cs | 0 Tests/UnitTests/ShogiShould.cs | 462 +++++++++++++++ Tests/UnitTests/UnitTests.csproj | 36 ++ azure-pipelines.yml | 18 +- 222 files changed, 6157 insertions(+), 3201 deletions(-) delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Types/SessionMetadata.cs delete mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs delete mode 100644 Gameboard.ShogiUI.Sockets.sln delete mode 100644 Gameboard.ShogiUI.Sockets/Controllers/GameController.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Models/User.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs delete mode 100644 Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs delete mode 100644 Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs delete mode 100644 Shogi.AcceptanceTests/AcceptanceTests.cs create mode 100644 Shogi.Contracts/Api/Commands/CreateGuestTokenResponse.cs create mode 100644 Shogi.Contracts/Api/Commands/CreateSessionCommand.cs create mode 100644 Shogi.Contracts/Api/Commands/CreateTokenResponse.cs create mode 100644 Shogi.Contracts/Api/Commands/MovePieceCommand.cs create mode 100644 Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs create mode 100644 Shogi.Contracts/Api/Queries/ReadSessionResponse.cs rename Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj => Shogi.Contracts/Shogi.Contracts.csproj (86%) create mode 100644 Shogi.Contracts/Socket/CreateGame.cs create mode 100644 Shogi.Contracts/Socket/ISocketRequest.cs create mode 100644 Shogi.Contracts/Socket/ISocketResponse.cs create mode 100644 Shogi.Contracts/Socket/Move.cs rename {Gameboard.ShogiUI.Sockets.ServiceModels => Shogi.Contracts}/Types/BoardState.cs (89%) rename {Gameboard.ShogiUI.Sockets.ServiceModels => Shogi.Contracts}/Types/Move.cs (85%) rename {Gameboard.ShogiUI.Sockets.ServiceModels => Shogi.Contracts}/Types/Piece.cs (73%) rename {Gameboard.ShogiUI.Sockets.ServiceModels => Shogi.Contracts}/Types/Session.cs (61%) create mode 100644 Shogi.Contracts/Types/SessionMetadata.cs create mode 100644 Shogi.Contracts/Types/SocketAction.cs rename {Gameboard.ShogiUI.Sockets.ServiceModels => Shogi.Contracts}/Types/User.cs (68%) create mode 100644 Shogi.Contracts/Types/WhichPerspective.cs rename {Gameboard.ShogiUI.Sockets.ServiceModels => Shogi.Contracts}/Types/WhichPiece.cs (69%) create mode 100644 Shogi.Database/Post Deployment/Script.PostDeployment.sql create mode 100644 Shogi.Database/Post Deployment/Scripts/PopulateLoginPlatforms.sql create mode 100644 Shogi.Database/Session/Session.sql create mode 100644 Shogi.Database/Session/Stored Procedures/CreateBoardState.sql create mode 100644 Shogi.Database/Session/Stored Procedures/CreateSession.sql create mode 100644 Shogi.Database/Session/Stored Procedures/ReadAllSessionsMetadata.sql create mode 100644 Shogi.Database/Session/Stored Procedures/UpdateSession.sql create mode 100644 Shogi.Database/Session/Tables/BoardState.sql create mode 100644 Shogi.Database/Session/Tables/Session.sql create mode 100644 Shogi.Database/Session/Types/JsonDocument.sql create mode 100644 Shogi.Database/Session/Types/SessionName.sql create mode 100644 Shogi.Database/Shogi.Database.sqlproj create mode 100644 Shogi.Database/User/StoredProcedures/CreateUser.sql create mode 100644 Shogi.Database/User/StoredProcedures/ReadUser.sql create mode 100644 Shogi.Database/User/Tables/LoginPlatform.sql create mode 100644 Shogi.Database/User/Tables/User.sql create mode 100644 Shogi.Database/User/Types/UserName.sql create mode 100644 Shogi.Database/User/User.sql delete mode 100644 Shogi.Domain.UnitTests/RookShould.cs delete mode 100644 Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj delete mode 100644 Shogi.Domain.UnitTests/ShogiShould.cs create mode 100644 Shogi.Domain/Aggregates/Session.cs delete mode 100644 Shogi.Domain/Pieces/Bishop.cs delete mode 100644 Shogi.Domain/Pieces/GoldGeneral.cs delete mode 100644 Shogi.Domain/Pieces/King.cs delete mode 100644 Shogi.Domain/Pieces/Knight.cs delete mode 100644 Shogi.Domain/Pieces/Lance.cs delete mode 100644 Shogi.Domain/Pieces/Pawn.cs delete mode 100644 Shogi.Domain/Pieces/Piece.cs delete mode 100644 Shogi.Domain/Pieces/Rook.cs delete mode 100644 Shogi.Domain/Pieces/SilverGeneral.cs delete mode 100644 Shogi.Domain/Session.cs delete mode 100644 Shogi.Domain/SessionMetadata.cs create mode 100644 Shogi.Domain/ValueObjects/Bishop.cs create mode 100644 Shogi.Domain/ValueObjects/GoldGeneral.cs create mode 100644 Shogi.Domain/ValueObjects/King.cs create mode 100644 Shogi.Domain/ValueObjects/Knight.cs create mode 100644 Shogi.Domain/ValueObjects/Lance.cs create mode 100644 Shogi.Domain/ValueObjects/Pawn.cs create mode 100644 Shogi.Domain/ValueObjects/Piece.cs create mode 100644 Shogi.Domain/ValueObjects/Rook.cs create mode 100644 Shogi.Domain/ValueObjects/SilverGeneral.cs rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/.config/dotnet-tools.json (100%) create mode 100644 Shogi.Sockets/Controllers/SessionController.cs create mode 100644 Shogi.Sockets/Controllers/UserController.cs rename Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs => Shogi.Sockets/ExampleAnonymousSessionMiddleware.cs (88%) create mode 100644 Shogi.Sockets/Extensions/Extensions.cs rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Extensions/LogMiddleware.cs (95%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Extensions/WebSocketExtensions.cs (81%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Managers/ModelMapper.cs (71%) create mode 100644 Shogi.Sockets/Managers/SocketConnectionManager.cs rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Managers/SocketTokenCache.cs (96%) create mode 100644 Shogi.Sockets/Models/User.cs rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Models/WhichLoginPlatform.cs (64%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Program.cs (63%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Properties/PublishProfiles/FolderProfile.pubxml (100%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Properties/ServiceDependencies/local/secrets1.arm.json (100%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Properties/launchSettings.json (100%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Properties/serviceDependencies.json (100%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Properties/serviceDependencies.local.json (100%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Readme.md (73%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/BoardStateDocument.cs (68%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/CouchCreatedResult.cs (79%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/CouchDocument.cs (92%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/CouchFindResult.cs (77%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/CouchViewResult.cs (87%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/Move.cs (91%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/Piece.cs (74%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/SessionDocument.cs (70%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/UserDocument.cs (84%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Repositories/CouchModels/WhichDocumentType.cs (56%) create mode 100644 Shogi.Sockets/Repositories/GameboardRepository.cs create mode 100644 Shogi.Sockets/Repositories/QueryRepository.cs create mode 100644 Shogi.Sockets/Repositories/SessionRepository.cs create mode 100644 Shogi.Sockets/Repositories/UserRepository.cs rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/Services/SocketService.cs (59%) rename Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj => Shogi.Sockets/Shogi.Api.csproj (69%) create mode 100644 Shogi.Sockets/ShogiUserClaimsTransformer.cs rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/appsettings.Development.json (100%) rename {Gameboard.ShogiUI.Sockets => Shogi.Sockets}/appsettings.json (60%) create mode 100644 Shogi.UI/.config/dotnet-tools.json create mode 100644 Shogi.UI/App.razor create mode 100644 Shogi.UI/Pages/Authentication.razor create mode 100644 Shogi.UI/Pages/Home/Account/AccountManager.cs create mode 100644 Shogi.UI/Pages/Home/Account/AccountState.cs create mode 100644 Shogi.UI/Pages/Home/Account/LocalStorageExtensions.cs create mode 100644 Shogi.UI/Pages/Home/Account/LoginEventArgs.cs create mode 100644 Shogi.UI/Pages/Home/Account/User.cs create mode 100644 Shogi.UI/Pages/Home/Account/WhichAccountPlatform.cs create mode 100644 Shogi.UI/Pages/Home/Api/CookieMessageHandler.cs create mode 100644 Shogi.UI/Pages/Home/Api/IShogiApi.cs create mode 100644 Shogi.UI/Pages/Home/Api/MsalMessageHandler.cs create mode 100644 Shogi.UI/Pages/Home/Api/ShogiApi.cs create mode 100644 Shogi.UI/Pages/Home/GameBoard.razor create mode 100644 Shogi.UI/Pages/Home/GameBoard.razor.css create mode 100644 Shogi.UI/Pages/Home/GameBrowser.razor create mode 100644 Shogi.UI/Pages/Home/GameBrowser.razor.css create mode 100644 Shogi.UI/Pages/Home/GamePiece.razor create mode 100644 Shogi.UI/Pages/Home/GamePiece.razor.css create mode 100644 Shogi.UI/Pages/Home/Home.razor create mode 100644 Shogi.UI/Pages/Home/Home.razor.css create mode 100644 Shogi.UI/Pages/Home/LoginModal.razor create mode 100644 Shogi.UI/Pages/Home/LoginModal.razor.css create mode 100644 Shogi.UI/Pages/Home/PageHeader.razor create mode 100644 Shogi.UI/Pages/Home/PageHeader.razor.css create mode 100644 Shogi.UI/Pages/Home/Pieces/Bishop.razor create mode 100644 Shogi.UI/Pages/Home/Pieces/ChallengingKing.razor create mode 100644 Shogi.UI/Pages/Home/Pieces/GoldGeneral.razor create mode 100644 Shogi.UI/Pages/Home/Pieces/Knight.razor create mode 100644 Shogi.UI/Pages/Home/Pieces/Lance.razor create mode 100644 Shogi.UI/Pages/Home/Pieces/Pawn.razor create mode 100644 Shogi.UI/Pages/Home/Pieces/ReigningKing.razor create mode 100644 Shogi.UI/Pages/Home/Pieces/Rook.razor create mode 100644 Shogi.UI/Pages/Home/Pieces/SilverGeneral.razor create mode 100644 Shogi.UI/Program.cs create mode 100644 Shogi.UI/Properties/Resources.Designer.cs create mode 100644 Shogi.UI/Properties/Resources.resx create mode 100644 Shogi.UI/Properties/launchSettings.json create mode 100644 Shogi.UI/Properties/serviceDependencies.json create mode 100644 Shogi.UI/Properties/serviceDependencies.local.json create mode 100644 Shogi.UI/Shared/LocalStorage.cs create mode 100644 Shogi.UI/Shared/LoginDisplay.razor create mode 100644 Shogi.UI/Shared/MainLayout.razor create mode 100644 Shogi.UI/Shared/MainLayout.razor.css create mode 100644 Shogi.UI/Shared/Modal/ModalService.cs create mode 100644 Shogi.UI/Shared/Modal/ModalVisibilityChangedEventArgs.cs create mode 100644 Shogi.UI/Shared/Modal/Modals.razor create mode 100644 Shogi.UI/Shared/Modal/Modals.razor.css create mode 100644 Shogi.UI/Shared/MyNotAuthorized.razor create mode 100644 Shogi.UI/Shared/RedirectToLogin.razor create mode 100644 Shogi.UI/Shared/RotatingCogsSvg.razor create mode 100644 Shogi.UI/Shared/RotatingCogsSvg.razor.css create mode 100644 Shogi.UI/Shared/ShogiSocket.cs create mode 100644 Shogi.UI/Shogi.UI.csproj create mode 100644 Shogi.UI/_Imports.razor create mode 100644 Shogi.UI/wwwroot/appsettings.Development.json create mode 100644 Shogi.UI/wwwroot/appsettings.json create mode 100644 Shogi.UI/wwwroot/css/app.css create mode 100644 Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.css create mode 100644 Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.css.map create mode 100644 Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.js create mode 100644 Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.js.map create mode 100644 Shogi.UI/wwwroot/css/open-iconic/FONT-LICENSE create mode 100644 Shogi.UI/wwwroot/css/open-iconic/ICON-LICENSE create mode 100644 Shogi.UI/wwwroot/css/open-iconic/README.md create mode 100644 Shogi.UI/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css create mode 100644 Shogi.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.eot create mode 100644 Shogi.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.otf create mode 100644 Shogi.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.svg create mode 100644 Shogi.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf create mode 100644 Shogi.UI/wwwroot/css/open-iconic/font/fonts/open-iconic.woff create mode 100644 Shogi.UI/wwwroot/favicon.ico create mode 100644 Shogi.UI/wwwroot/icon-192.png create mode 100644 Shogi.UI/wwwroot/index.html create mode 100644 Shogi.UI/wwwroot/sample-data/weather.json create mode 100644 Shogi.sln create mode 100644 Tests/AcceptanceTests/AcceptanceTests.cs rename Shogi.AcceptanceTests/Shogi.AcceptanceTests.csproj => Tests/AcceptanceTests/AcceptanceTests.csproj (95%) rename Shogi.AcceptanceTests/TestSetup/AATFixture.cs => Tests/AcceptanceTests/TestSetup/MsalTestFixture - Copy.cs (67%) create mode 100644 Tests/AcceptanceTests/TestSetup/MsalTestFixture.cs rename {Shogi.AcceptanceTests => Tests/AcceptanceTests}/Usings.cs (100%) rename {Shogi.AcceptanceTests => Tests/AcceptanceTests}/appsettings.json (100%) rename {Shogi.Domain.UnitTests => Tests/UnitTests}/NotationShould.cs (92%) create mode 100644 Tests/UnitTests/RookShould.cs create mode 100644 Tests/UnitTests/ServiceModels/ServiceModelsShouldSerialize.cs rename {Shogi.Domain.UnitTests => Tests/UnitTests}/ShogiBoardStateShould.cs (100%) create mode 100644 Tests/UnitTests/ShogiShould.cs create mode 100644 Tests/UnitTests/UnitTests.csproj diff --git a/.gitignore b/.gitignore index 70b8a69..832cf47 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,5 @@ Thumbs.db bin obj *.user +/Shogi.Database/Shogi.Database.dbmdl +/Shogi.Database/Shogi.Database.jfm diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs deleted file mode 100644 index 90fc455..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetGuestToken.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api -{ - public class GetGuestTokenResponse - { - public string UserId { get; } - public string DisplayName { get; } - public Guid OneTimeToken { get; } - - public GetGuestTokenResponse(string id, string displayName, Guid token) - { - UserId = id; - DisplayName = displayName; - OneTimeToken = token; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs deleted file mode 100644 index 1a43c63..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetSessions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.Collections.Generic; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api -{ - public class GetSessionsResponse - { - public IList PlayerHasJoinedSessions { get; set; } - public IList AllOtherSessions { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs deleted file mode 100644 index acc03a9..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/GetToken.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api -{ - public class GetTokenResponse - { - public Guid OneTimeToken { get; } - - public GetTokenResponse(Guid token) - { - OneTimeToken = token; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs deleted file mode 100644 index acb270f..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostGameInvitation.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api -{ - public class PostGameInvitation - { - public string SessionName { get; set; } - } - public class PostGuestGameInvitation - { - public string GuestId { get; set; } - public string SessionName { get; set; } - } - public class PostGameInvitationResponse - { - public string Code { get; } - public PostGameInvitationResponse(string code) - { - Code = code; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs deleted file mode 100644 index 012c6ea..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostMove.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using System.ComponentModel.DataAnnotations; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api -{ - public class PostMove - { - [Required] - public Move Move { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs deleted file mode 100644 index ebe9665..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/PostSession.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Api -{ - public class PostSession - { - public string Name { get; set; } - public bool IsPrivate { get; set; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs deleted file mode 100644 index 306e3f1..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/CreateGame.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket -{ - public class CreateGameResponse : ISocketResponse - { - public string Action { get; } - public SessionMetadata Game { get; set; } - - /// - /// The player who created the game. - /// - public string PlayerName { get; set; } - - public CreateGameResponse() - { - Action = ClientAction.CreateGame.ToString(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs deleted file mode 100644 index aca4cc7..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket -{ - public interface IRequest - { - ClientAction Action { get; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs deleted file mode 100644 index f72caaa..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/IResponse.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket -{ - public interface ISocketResponse - { - string Action { get; } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs deleted file mode 100644 index fc4f786..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/JoinGame.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 : ISocketResponse - { - public string Action { get; protected set; } - public string GameName { get; set; } - /// - /// The player who joined the game. - /// - public string PlayerName { get; set; } - - public JoinGameResponse() - { - Action = ClientAction.JoinGame.ToString(); - GameName = ""; - PlayerName = ""; - } - } - - public class JoinByCodeResponse : JoinGameResponse, ISocketResponse - { - public JoinByCodeResponse() - { - Action = ClientAction.JoinByCode.ToString(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs deleted file mode 100644 index f5a3fb1..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Move.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Socket -{ - public class MoveResponse : ISocketResponse - { - public string Action { get; } - public string GameName { get; set; } - /// - /// The player that made the move. - /// - public string PlayerName { get; set; } - - public MoveResponse() - { - Action = ClientAction.Move.ToString(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs deleted file mode 100644 index 99b9446..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/ClientActionEnum.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types -{ - public enum ClientAction - { - CreateGame, - JoinGame, - JoinByCode, - Move - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/SessionMetadata.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/SessionMetadata.cs deleted file mode 100644 index 660ebe0..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/SessionMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types -{ - public class SessionMetadata - { - public string Name { get; set; } - public string Player1 { get; set; } - public string? Player2 { get; set; } - public bool IsPrivate { get; set; } - - public string[] Players => string.IsNullOrEmpty(Player2) ? new[] { Player1 } : new[] { Player1, Player2 }; - } -} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs deleted file mode 100644 index 3fb839b..0000000 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPerspective.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types -{ - public enum WhichPlayer - { - Player1, - Player2 - } -} diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln deleted file mode 100644 index 4c90352..0000000 --- a/Gameboard.ShogiUI.Sockets.sln +++ /dev/null @@ -1,55 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets", "Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj", "{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.ServiceModels", "Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj", "{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain", "Shogi.Domain\Shogi.Domain.csproj", "{0211B1E4-20F0-4058-AAC4-3845D19910AF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Domain.UnitTests", "Shogi.Domain.UnitTests\Shogi.Domain.UnitTests.csproj", "{F256989E-B6AF-4731-9DB4-88991C40B2CE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.AcceptanceTests", "Shogi.AcceptanceTests\Shogi.AcceptanceTests.csproj", "{F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Release|Any CPU.Build.0 = Release|Any CPU - {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.ActiveCfg = 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.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 - {F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {F256989E-B6AF-4731-9DB4-88991C40B2CE} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} - {F4AB1C7C-CDE5-465D-81A1-DAF1D97225BA} = {F35A56FB-B8D8-4CB7-ABF6-D40049C9B42E} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} - EndGlobalSection -EndGlobal diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs deleted file mode 100644 index ef5f6e1..0000000 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ /dev/null @@ -1,253 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Managers; -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Api; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Gameboard.ShogiUI.Sockets.Controllers -{ - - [ApiController] - [Route("[controller]")] - [Authorize] - public class GameController : ControllerBase - { - private readonly IGameboardManager gameboardManager; - private readonly IGameboardRepository gameboardRepository; - private readonly ISocketConnectionManager communicationManager; - private readonly IModelMapper mapper; - - public GameController( - IGameboardRepository repository, - IGameboardManager manager, - ISocketConnectionManager communicationManager, - IModelMapper mapper) - { - gameboardManager = manager; - gameboardRepository = repository; - this.communicationManager = communicationManager; - this.mapper = mapper; - } - - [HttpPost("JoinCode")] - public async Task PostGameInvitation([FromBody] PostGameInvitation request) - { - - //var isPlayer1 = await gameboardManager.IsPlayer1(request.SessionName, userName); - //if (isPlayer1) - //{ - // var code = await gameboardRepository.PostJoinCode(request.SessionName, userName); - // return new CreatedResult("", new PostGameInvitationResponse(code)); - //} - //else - //{ - return new UnauthorizedResult(); - //} - } - - [AllowAnonymous] - [HttpPost("GuestJoinCode")] - public async Task PostGuestGameInvitation([FromBody] PostGuestGameInvitation request) - { - - //var isGuest = gameboardManager.IsGuest(request.GuestId); - //var isPlayer1 = gameboardManager.IsPlayer1(request.SessionName, request.GuestId); - //if (isGuest && await isPlayer1) - //{ - // var code = await gameboardRepository.PostJoinCode(request.SessionName, request.GuestId); - // return new CreatedResult("", new PostGameInvitationResponse(code)); - //} - //else - //{ - return new UnauthorizedResult(); - //} - } - - [HttpPost("{gameName}/Move")] - public async Task PostMove([FromRoute] string gameName, [FromBody] PostMove request) - { - var user = await gameboardManager.ReadUser(User); - var session = await gameboardRepository.ReadSession(gameName); - if (session == null) - { - return NotFound(); - } - if (user == null || (session.Player1Name != user.Id && session.Player2Name != user.Id)) - { - return Forbid("User is not seated at this game."); - } - - try - { - var move = request.Move; - if (move.PieceFromCaptured.HasValue) - session.Move(mapper.Map(move.PieceFromCaptured.Value), move.To); - else if (!string.IsNullOrWhiteSpace(move.From)) - session.Move(move.From, move.To, move.IsPromotion); - - await gameboardRepository.CreateBoardState(session); - await communicationManager.BroadcastToPlayers( - new MoveResponse - { - GameName = session.Name, - PlayerName = user.Id - }, - session.Player1Name, - session.Player2Name); - - return Ok(); - } - catch (InvalidOperationException ex) - { - return Conflict(ex.Message); - } - } - - // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. - //[Route("")] - //public async Task PostSession([FromBody] PostSession request) - //{ - // var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2); - // var success = await repository.CreateSession(model); - // if (success) - // { - // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.ClientAction.CreateGame) - // { - // Game = model.ToServiceModel(), - // PlayerName = - // } - // var task = request.IsPrivate - // ? communicationManager.BroadcastToPlayers(response, userName) - // : communicationManager.BroadcastToAll(response); - // return new CreatedResult("", null); - // } - // return new ConflictResult(); - //} - - [HttpPost] - public async Task PostSession([FromBody] PostSession request) - { - var user = await ReadUserOrThrow(); - var session = new Shogi.Domain.SessionMetadata(request.Name, request.IsPrivate, user.Id); - await gameboardRepository.CreateSession(session); - await communicationManager.BroadcastToAll(new CreateGameResponse - { - Game = mapper.Map(session), - PlayerName = user.Id - }); - - return Ok(); - - } - - /// - /// Reads the board session and subscribes the caller to socket events for that session. - /// - [HttpGet("{gameName}")] - public async Task GetSession([FromRoute] string gameName) - { - var user = await ReadUserOrThrow(); - var session = await gameboardRepository.ReadSession(gameName); - if (session == null) - { - return NotFound(); - } - - var playerPerspective = session.Player2Name == user.Id - ? WhichPlayer.Player2 - : WhichPlayer.Player1; - - var response = new Session - { - BoardState = new BoardState - { - Board = mapper.Map(session.BoardState), - Player1Hand = session.Player1Hand.Select(mapper.Map).ToList(), - Player2Hand = session.Player2Hand.Select(mapper.Map).ToList(), - PlayerInCheck = mapper.Map(session.InCheck) - }, - GameName = session.Name, - Player1 = session.Player1Name, - Player2 = session.Player2Name - }; - return Ok(response); - } - - [HttpGet] - public async Task> GetSessions() - { - var user = await ReadUserOrThrow(); - var sessions = await gameboardRepository.ReadSessionMetadatas(); - - var sessionsJoinedByUser = sessions - .Where(s => s.IsSeated(user.Id)) - .Select(s => mapper.Map(s)) - .ToList(); - var sessionsNotJoinedByUser = sessions - .Where(s => !s.IsSeated(user.Id)) - .Select(s => mapper.Map(s)) - .ToList(); - - return Ok(new GetSessionsResponse - { - PlayerHasJoinedSessions = sessionsJoinedByUser, - AllOtherSessions = sessionsNotJoinedByUser - }); - } - - [HttpPut("{gameName}")] - public async Task PutJoinSession([FromRoute] string gameName) - { - var user = await ReadUserOrThrow(); - var session = await gameboardRepository.ReadSessionMetaData(gameName); - if (session == null) - { - return NotFound(); - } - if (session.Player2 != null) - { - return this.Conflict("This session already has two seated players and is full."); - } - - session.SetPlayer2(user.Id); - await gameboardRepository.UpdateSession(session); - - var opponentName = user.Id == session.Player1 - ? session.Player2! - : session.Player1; - await communicationManager.BroadcastToPlayers(new JoinGameResponse - { - GameName = session.Name, - PlayerName = user.Id - }, opponentName); - return Ok(); - } - - [Authorize(Roles = "Admin, Shogi")] - [HttpDelete("{gameName}")] - public async Task DeleteSession([FromRoute] string gameName) - { - var user = await ReadUserOrThrow(); - if (user.IsAdmin) - { - return Ok(); - } - else - { - return Unauthorized(); - } - } - - private async Task ReadUserOrThrow() - { - var user = await gameboardManager.ReadUser(User); - if (user == null) - { - throw new UnauthorizedAccessException("Unknown user claims."); - } - return user; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs deleted file mode 100644 index 1786b8e..0000000 --- a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Managers; -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Api; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; - -namespace Gameboard.ShogiUI.Sockets.Controllers -{ - [ApiController] - [Route("[controller]")] - [Authorize] - public class SocketController : ControllerBase - { - private readonly ISocketTokenCache tokenCache; - private readonly IGameboardManager gameboardManager; - private readonly IGameboardRepository gameboardRepository; - private readonly ISocketConnectionManager connectionManager; - private readonly AuthenticationProperties authenticationProps; - - public SocketController( - ILogger logger, - ISocketTokenCache tokenCache, - IGameboardManager gameboardManager, - IGameboardRepository gameboardRepository, - ISocketConnectionManager connectionManager) - { - this.tokenCache = tokenCache; - this.gameboardManager = gameboardManager; - this.gameboardRepository = gameboardRepository; - this.connectionManager = connectionManager; - - authenticationProps = new AuthenticationProperties - { - AllowRefresh = true, - IsPersistent = true - }; - } - - [HttpGet("GuestLogout")] - [AllowAnonymous] - public async Task GuestLogout() - { - var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - - var userId = User?.UserId(); - if (!string.IsNullOrEmpty(userId)) - { - connectionManager.Unsubscribe(userId); - } - - await signoutTask; - return Ok(); - } - - [HttpGet("Token")] - public async Task GetToken() - { - var user = await gameboardManager.ReadUser(User); - if (user == null) - { - await gameboardManager.CreateUser(User); - user = await gameboardManager.ReadUser(User); - } - - if (user == null) - { - return Unauthorized(); - } - - var token = tokenCache.GenerateToken(user.Id); - return new JsonResult(new GetTokenResponse(token)); - } - - [HttpGet("GuestToken")] - [AllowAnonymous] - public async Task GetGuestToken() - { - var user = await gameboardManager.ReadUser(User); - if (user == null) - { - // Create a guest user. - var newUser = Models.User.CreateGuestUser(Guid.NewGuid().ToString()); - await gameboardRepository.CreateUser(newUser); - - var identity = newUser.CreateClaimsIdentity(); - await HttpContext.SignInAsync( - CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(identity), - authenticationProps - ); - user = newUser; - } - - var token = tokenCache.GenerateToken(user.Id.ToString()); - return this.Ok(new GetGuestTokenResponse(user.Id, user.DisplayName, token)); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs deleted file mode 100644 index d687b10..0000000 --- a/Gameboard.ShogiUI.Sockets/Extensions/Extensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Security.Claims; - -namespace Gameboard.ShogiUI.Sockets.Extensions -{ - public static class Extensions - { - private static readonly string MsalUsernameClaim = "preferred_username"; - - 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 IsMicrosoft(this ClaimsPrincipal self) - { - return self.HasClaim(c => c.Type == MsalUsernameClaim); - } - - public static string ChangeFirstCharacterToLowerCase(this string self) - { - return char.ToLowerInvariant(self[0]) + self[1..]; - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs deleted file mode 100644 index 2dcac05..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Repositories; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers -{ - public interface IJoinByCodeHandler - { - Task Handle(JoinByCodeRequest request, string userName); - } - public class JoinByCodeHandler : IJoinByCodeHandler - { - private readonly IGameboardRepository repository; - private readonly ISocketConnectionManager communicationManager; - - public JoinByCodeHandler( - ISocketConnectionManager communicationManager, - IGameboardRepository repository) - { - this.repository = repository; - this.communicationManager = communicationManager; - } - - public async Task Handle(JoinByCodeRequest request, string userName) - { - //var request = JsonConvert.DeserializeObject(json); - //var sessionName = await repository.PostJoinPrivateSession(new PostJoinPrivateSession - //{ - // PlayerName = userName, - // JoinCode = request.JoinCode - //}); - - //if (sessionName == null) - //{ - // var response = new JoinGameResponse(ClientAction.JoinByCode) - // { - // PlayerName = userName, - // GameName = sessionName, - // Error = "Error joining game." - // }; - // await communicationManager.BroadcastToPlayers(response, userName); - //} - //else - //{ - // // Other members of the game see a regular JoinGame occur. - // var response = new JoinGameResponse(ClientAction.JoinGame) - // { - // PlayerName = userName, - // GameName = sessionName - // }; - // // At this time, userName hasn't subscribed and won't receive this message. - // await communicationManager.BroadcastToGame(sessionName, response); - - // // The player joining sees the JoinByCode occur. - // response = new JoinGameResponse(ClientAction.JoinByCode) - // { - // PlayerName = userName, - // GameName = sessionName - // }; - // await communicationManager.BroadcastToPlayers(response, userName); - //} - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs b/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs deleted file mode 100644 index fe315f4..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/GameboardManager.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Models; -using Gameboard.ShogiUI.Sockets.Repositories; -using System; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers -{ - public interface IGameboardManager - { - Task AssignPlayer2ToSession(string sessionName, User user); - Task ReadUser(ClaimsPrincipal user); - Task CreateUser(ClaimsPrincipal user); - } - - public class GameboardManager : IGameboardManager - { - private readonly IGameboardRepository repository; - - public GameboardManager(IGameboardRepository repository) - { - this.repository = repository; - } - - public async Task CreateUser(ClaimsPrincipal principal) - { - var id = principal.UserId(); - if (string.IsNullOrEmpty(id)) - { - throw new InvalidOperationException("Cannot create user from given claims."); - } - - var user = principal.IsMicrosoft() - ? User.CreateMsalUser(id) - : User.CreateGuestUser(id); - - await repository.CreateUser(user); - return user; - } - - public Task ReadUser(ClaimsPrincipal principal) - { - var userId = principal.UserId(); - if (!string.IsNullOrEmpty(userId)) - { - return repository.ReadUser(userId); - } - - return Task.FromResult(null); - } - - - public async Task CreateJoinCode(string sessionName, string playerName) - { - //var session = await repository.GetGame(sessionName); - //if (playerName == session?.Player1) - //{ - // return await repository.PostJoinCode(sessionName, playerName); - //} - return string.Empty; - } - - public async Task AssignPlayer2ToSession(string sessionName, User user) - { - var session = await repository.ReadSessionMetaData(sessionName); - if (session != null && !session.IsPrivate && session.Player2 == null) - { - session.SetPlayer2(user.Id); - await repository.UpdateSession(session); - } - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs deleted file mode 100644 index 6c35ccf..0000000 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Net.WebSockets; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Managers -{ - public interface ISocketConnectionManager - { - Task BroadcastToAll(ISocketResponse response); - void Subscribe(WebSocket socket, string playerName); - void Unsubscribe(string playerName); - Task BroadcastToPlayers(ISocketResponse response, params string?[] playerNames); - } - - /// - /// Retains all active socket connections and provides convenient methods for sending messages to clients. - /// - public class SocketConnectionManager : ISocketConnectionManager - { - /// Dictionary key is player name. - private readonly ConcurrentDictionary connections; - /// Dictionary key is game name. - private readonly ILogger logger; - - public SocketConnectionManager(ILogger logger) - { - this.logger = logger; - connections = new ConcurrentDictionary(); - } - - public void Subscribe(WebSocket socket, string playerName) - { - connections.TryRemove(playerName, out var _); - connections.TryAdd(playerName, socket); - } - - public void Unsubscribe(string playerName) - { - connections.TryRemove(playerName, out _); - } - - public async Task BroadcastToPlayers(ISocketResponse response, params string?[] playerNames) - { - var tasks = new List(playerNames.Length); - foreach (var name in playerNames) - { - if (!string.IsNullOrEmpty(name) && connections.TryGetValue(name, out var socket)) - { - var serialized = JsonConvert.SerializeObject(response); - logger.LogInformation("Response to {0} \n{1}\n", name, serialized); - tasks.Add(socket.SendTextAsync(serialized)); - } - } - await Task.WhenAll(tasks); - } - public Task BroadcastToAll(ISocketResponse response) - { - var message = JsonConvert.SerializeObject(response); - logger.LogInformation($"Broadcasting\n{0}", message); - var tasks = new List(connections.Count); - foreach (var kvp in connections) - { - var socket = kvp.Value; - try - { - - tasks.Add(socket.SendTextAsync(message)); - } - catch (WebSocketException) - { - logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); - Unsubscribe(kvp.Key); - } - catch - { - logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); - Unsubscribe(kvp.Key); - } - } - try - { - var task = Task.WhenAll(tasks); - return task; - } - catch - { - Console.WriteLine("Yo"); - } - return Task.FromResult(0); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs deleted file mode 100644 index ebc2da0..0000000 --- a/Gameboard.ShogiUI.Sockets/Models/User.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Repositories.CouchModels; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using System.Collections.ObjectModel; -using System.Security.Claims; - -namespace Gameboard.ShogiUI.Sockets.Models -{ - public class User - { - public static readonly ReadOnlyCollection Adjectives = new(new[] { - "Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly" - }); - public static readonly ReadOnlyCollection Subjects = new(new[] { - "Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato", "Penguin" - }); - public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft); - public static User CreateGuestUser(string id) - { - var random = new Random(); - // Adjective - var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count); - var adj = Adjectives[index]; - // Subject - index = (int)Math.Floor(random.NextDouble() * Subjects.Count); - var subj = Subjects[index]; - - return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest); - } - - public string Id { get; } - public string DisplayName { get; } - - public WhichLoginPlatform LoginPlatform { get; } - - public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest; - - public bool IsAdmin => LoginPlatform == WhichLoginPlatform.Microsoft && Id == "Hauth@live.com"; - - public User(string id, string displayName, WhichLoginPlatform platform) - { - Id = id; - DisplayName = displayName; - LoginPlatform = platform; - } - - public User(UserDocument document) - { - Id = document.Id; - DisplayName = document.DisplayName; - LoginPlatform = document.Platform; - } - - public ClaimsIdentity CreateClaimsIdentity() - { - if (LoginPlatform == WhichLoginPlatform.Guest) - { - var claims = new List(4) - { - new Claim(ClaimTypes.NameIdentifier, Id), - new Claim(ClaimTypes.Name, DisplayName), - new Claim(ClaimTypes.Role, "Guest"), - }; - return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - } - else - { - var claims = new List(3) - { - new Claim(ClaimTypes.NameIdentifier, Id), - new Claim(ClaimTypes.Name, DisplayName), - }; - return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme); - } - } - - public ServiceModels.Types.User ToServiceModel() => new() - { - Id = Id, - Name = DisplayName - }; - } -} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs deleted file mode 100644 index 2f932c7..0000000 --- a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs +++ /dev/null @@ -1,310 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Repositories.CouchModels; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Shogi.Domain; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -namespace Gameboard.ShogiUI.Sockets.Repositories -{ - public interface IGameboardRepository - { - Task CreateBoardState(Session session); - Task CreateSession(SessionMetadata session); - Task CreateUser(Models.User user); - Task> ReadSessionMetadatas(); - Task ReadSession(string name); - Task UpdateSession(SessionMetadata session); - Task ReadSessionMetaData(string name); - Task ReadUser(string userName); - } - - public class GameboardRepository : IGameboardRepository - { - /// - /// Returns session, board state, and user documents, grouped by session. - /// - private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate"; - /// - /// Returns session and user documents, grouped by session. - /// - private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata"; - private static readonly string View_User = "_design/user/_view/user"; - private const string ApplicationJson = "application/json"; - private readonly HttpClient client; - private readonly ILogger logger; - - public GameboardRepository(IHttpClientFactory clientFactory, ILogger logger) - { - client = clientFactory.CreateClient("couchdb"); - this.logger = logger; - } - - public async Task> ReadSessionMetadatas() - { - var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString(); - var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); - var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - if (result != null) - { - var groupedBySession = result.rows.GroupBy(row => row.id); - var sessions = new List(result.total_rows / 3); - foreach (var group in groupedBySession) - { - /** - * A group contains 3 elements. - * 1) The session metadata. - * 2) User document of Player1. - * 3) User document of Player2. - */ - var session = group.FirstOrDefault()?.doc.ToObject(); - var player1 = group.Skip(1).FirstOrDefault()?.doc.ToObject(); - var player2Doc = group.Skip(2).FirstOrDefault()?.doc; - if (session != null && player1 != null && player2Doc != null) - { - var player2 = IsUserDocument(player2Doc) - ? new Models.User(player2Doc.ToObject()!) - : null; - sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id)); - } - } - return new Collection(sessions); - } - return new Collection(Array.Empty()); - } - - private static bool IsUserDocument(JObject player2Doc) - { - return player2Doc?.SelectToken(nameof(CouchDocument.DocumentType))?.Value() == WhichDocumentType.User; - } - - public async Task ReadSession(string name) - { - static Shogi.Domain.Pieces.Piece? MapPiece(Piece? piece) - { - return piece == null - ? null - : Shogi.Domain.Pieces.Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); - } - - var queryParams = new QueryBuilder - { - { "include_docs", "true" }, - { "startkey", JsonConvert.SerializeObject(new [] {name}) }, - { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } - }.ToQueryString(); - var query = $"{View_SessionWithBoardState}{queryParams}"; - logger.LogInformation("ReadSession() query: {query}", query); - var response = await client.GetAsync(query); - var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - if (result != null && result.rows.Length > 2) - { - var group = result.rows; - /** - * A group contains multiple elements. - * 0) The session metadata. - * 1) User documents of Player1. - * 2) User documents of Player1. - * 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :( - * Everything Else) Snapshots of the boardstate after every player move. - */ - var session = group[0].doc.ToObject(); - var player1 = group[1].doc.ToObject(); - var player2Doc = group[2].doc; - var boardState = group.Last().doc.ToObject(); - - if (session != null && player1 != null && boardState != null) - { - var player2 = IsUserDocument(player2Doc) - ? new Models.User(player2Doc.ToObject()!) - : null; - var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id); - var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value))); - return new Session(shogiBoardState, metaData); - } - } - return null; - } - - public async Task ReadSessionMetaData(string name) - { - var queryParams = new QueryBuilder - { - { "include_docs", "true" }, - { "startkey", JsonConvert.SerializeObject(new [] {name}) }, - { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } - }.ToQueryString(); - var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); - var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - if (result != null && result.rows.Length > 2) - { - var group = result.rows; - /** - * A group contains 3 elements. - * 1) The session metadata. - * 2) User document of Player1. - * 3) User document of Player2. - */ - var session = group[0].doc.ToObject(); - var player1 = group[1].doc.ToObject(); - var player2Doc = group[2].doc; - if (session != null && player1 != null) - { - var player2 = IsUserDocument(player2Doc) - ? new Models.User(player2Doc.ToObject()!) - : null; - return new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id); - } - } - return null; - } - - /// - /// Saves a snapshot of board state and the most recent move. - /// - public async Task CreateBoardState(Session session) - { - var boardStateDocument = new BoardStateDocument(session.Name, session); - var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync(string.Empty, content); - response.EnsureSuccessStatusCode(); - } - - public async Task CreateSession(SessionMetadata session) - { - var sessionDocument = new SessionDocument(session); - var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson); - var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent); - - var boardStateDocument = new BoardStateDocument(session.Name, new Session()); - var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); - - if ((await postSessionDocumentTask).IsSuccessStatusCode) - { - var response = await client.PostAsync(string.Empty, boardStateContent); - response.EnsureSuccessStatusCode(); - } - } - - public async Task UpdateSession(SessionMetadata session) - { - // GET existing session to get revisionId. - var readResponse = await client.GetAsync(session.Name); - readResponse.EnsureSuccessStatusCode(); - var sessionDocument = JsonConvert.DeserializeObject(await readResponse.Content.ReadAsStringAsync()); - - // PUT the document with the revisionId. - var couchModel = new SessionDocument(session) - { - RevisionId = sessionDocument?.RevisionId - }; - var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); - var response = await client.PutAsync(couchModel.Id, content); - response.EnsureSuccessStatusCode(); - } - //public async Task PutJoinPublicSession(PutJoinPublicSession request) - //{ - // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - // var response = await client.PutAsync(JoinSessionRoute, content); - // var json = await response.Content.ReadAsStringAsync(); - // return JsonConvert.DeserializeObject(json).JoinSucceeded; - //} - - //public async Task PostJoinPrivateSession(PostJoinPrivateSession request) - //{ - // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - // var response = await client.PostAsync(JoinSessionRoute, content); - // var json = await response.Content.ReadAsStringAsync(); - // var deserialized = JsonConvert.DeserializeObject(json); - // if (deserialized.JoinSucceeded) - // { - // return deserialized.SessionName; - // } - // return null; - //} - - //public async Task> GetMoves(string gameName) - //{ - // var uri = $"Session/{gameName}/Moves"; - // var get = await client.GetAsync(Uri.EscapeUriString(uri)); - // var json = await get.Content.ReadAsStringAsync(); - // if (string.IsNullOrWhiteSpace(json)) - // { - // return new List(); - // } - // var response = JsonConvert.DeserializeObject(json); - // return response.Moves.Select(m => new Move(m)).ToList(); - //} - - //public async Task PostMove(string gameName, PostMove request) - //{ - // var uri = $"Session/{gameName}/Move"; - // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); - // await client.PostAsync(Uri.EscapeUriString(uri), content); - //} - - public async Task PostJoinCode(string gameName, string userName) - { - // var uri = $"JoinCode/{gameName}"; - // var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); - // var content = new StringContent(serialized, Encoding.UTF8, MediaType); - // var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); - // return JsonConvert.DeserializeObject(json).JoinCode; - return string.Empty; - } - - public async Task ReadUser(string id) - { - var queryParams = new QueryBuilder - { - { "include_docs", "true" }, - { "key", JsonConvert.SerializeObject(id) }, - }.ToQueryString(); - var response = await client.GetAsync($"{View_User}{queryParams}"); - var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(responseContent); - if (result != null && result.rows.Length > 0) - { - return new Models.User(result.rows[0].doc); - } - - return null; - } - - public async Task CreateUser(Models.User user) - { - var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform); - var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); - var response = await client.PostAsync(string.Empty, content); - response.EnsureSuccessStatusCode(); - } - - public void ReadMoveHistory() - { - //TODO: Separate move history into a separate request. - //var moves = group - // .Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session. - // // TODO: Deserialize just the Move property. - // .Select(row => row.doc.ToObject()) - // .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(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs deleted file mode 100644 index e1de3a2..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinByCodeRequestValidator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators -{ - public class JoinByCodeRequestValidator : AbstractValidator - { - public JoinByCodeRequestValidator() - { - RuleFor(_ => _.Action).Equal(ClientAction.JoinByCode); - RuleFor(_ => _.JoinCode).NotEmpty(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs b/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs deleted file mode 100644 index da598e3..0000000 --- a/Gameboard.ShogiUI.Sockets/Services/RequestValidators/JoinGameRequestValidator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentValidation; -using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; - -namespace Gameboard.ShogiUI.Sockets.Services.RequestValidators -{ - public class JoinGameRequestValidator : AbstractValidator - { - public JoinGameRequestValidator() - { - RuleFor(_ => _.Action).Equal(ClientAction.JoinGame); - RuleFor(_ => _.GameName).NotEmpty(); - } - } -} diff --git a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs b/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs deleted file mode 100644 index c2b5977..0000000 --- a/Gameboard.ShogiUI.Sockets/ShogiUserClaimsTransformer.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Gameboard.ShogiUI.Sockets.Extensions; -using Gameboard.ShogiUI.Sockets.Repositories; -using Microsoft.AspNetCore.Authentication; -using System.Security.Claims; - -namespace Gameboard.ShogiUI.Sockets -{ - /// - /// Standardizes the claims from third party issuers. Also registers new msal users in the database. - /// - public class ShogiUserClaimsTransformer : IClaimsTransformation - { - private readonly IGameboardRepository gameboardRepository; - - public ShogiUserClaimsTransformer(IGameboardRepository gameboardRepository) - { - this.gameboardRepository = gameboardRepository; - } - - public async Task TransformAsync(ClaimsPrincipal principal) - { - var id = principal.UserId(); - if (!string.IsNullOrWhiteSpace(id)) - { - var user = await gameboardRepository.ReadUser(id); - if (user == null) - { - var newUser = principal.IsMicrosoft() - ? Models.User.CreateMsalUser(id) - : Models.User.CreateGuestUser(id); - - await gameboardRepository.CreateUser(newUser); - user = newUser; - } - - if (user != null) - { - return new ClaimsPrincipal(user.CreateClaimsIdentity()); - } - } - return principal; - } - } -} diff --git a/Shogi.AcceptanceTests/AcceptanceTests.cs b/Shogi.AcceptanceTests/AcceptanceTests.cs deleted file mode 100644 index d75ea58..0000000 --- a/Shogi.AcceptanceTests/AcceptanceTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Shogi.AcceptanceTests.TestSetup; -using Xunit.Abstractions; - -namespace Shogi.AcceptanceTests -{ - public class AcceptanceTests : IClassFixture - { - private readonly AATFixture fixture; - private readonly ITestOutputHelper console; - - public AcceptanceTests(AATFixture fixture, ITestOutputHelper console) - { - this.fixture = fixture; - this.console = console; - } - - [Fact] - public async Task CreateAndReadSession() - { - var response = await fixture.Service.GetAsync(new Uri("Game", UriKind.Relative)); - console.WriteLine(await response.Content.ReadAsStringAsync()); - console.WriteLine(response.Headers.WwwAuthenticate.ToString()); - response.IsSuccessStatusCode.Should().BeTrue(because: "AAT Client should be authorized."); - } - } -} \ No newline at end of file diff --git a/Shogi.Contracts/Api/Commands/CreateGuestTokenResponse.cs b/Shogi.Contracts/Api/Commands/CreateGuestTokenResponse.cs new file mode 100644 index 0000000..c72d12a --- /dev/null +++ b/Shogi.Contracts/Api/Commands/CreateGuestTokenResponse.cs @@ -0,0 +1,17 @@ +using System; + +namespace Shogi.Contracts.Api; + + public class CreateGuestTokenResponse + { + public string UserId { get; } + public string DisplayName { get; } + public Guid OneTimeToken { get; } + + public CreateGuestTokenResponse(string userId, string displayName, Guid oneTimeToken) + { + UserId = userId; + DisplayName = displayName; + OneTimeToken = oneTimeToken; + } + } diff --git a/Shogi.Contracts/Api/Commands/CreateSessionCommand.cs b/Shogi.Contracts/Api/Commands/CreateSessionCommand.cs new file mode 100644 index 0000000..3bbbde1 --- /dev/null +++ b/Shogi.Contracts/Api/Commands/CreateSessionCommand.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Shogi.Contracts.Api; + +public class CreateSessionCommand +{ + [Required] + public string Name { get; set; } = string.Empty; + public bool IsPrivate { get; set; } +} diff --git a/Shogi.Contracts/Api/Commands/CreateTokenResponse.cs b/Shogi.Contracts/Api/Commands/CreateTokenResponse.cs new file mode 100644 index 0000000..feea0a3 --- /dev/null +++ b/Shogi.Contracts/Api/Commands/CreateTokenResponse.cs @@ -0,0 +1,13 @@ +using System; + +namespace Shogi.Contracts.Api; + + public class CreateTokenResponse + { + public Guid OneTimeToken { get; } + + public CreateTokenResponse(Guid token) + { + OneTimeToken = token; + } + } diff --git a/Shogi.Contracts/Api/Commands/MovePieceCommand.cs b/Shogi.Contracts/Api/Commands/MovePieceCommand.cs new file mode 100644 index 0000000..aa9082e --- /dev/null +++ b/Shogi.Contracts/Api/Commands/MovePieceCommand.cs @@ -0,0 +1,10 @@ +using Shogi.Contracts.Types; +using System.ComponentModel.DataAnnotations; + +namespace Shogi.Contracts.Api; + + public class MovePieceCommand + { + [Required] + public Move Move { get; set; } + } diff --git a/Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs b/Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs new file mode 100644 index 0000000..4956250 --- /dev/null +++ b/Shogi.Contracts/Api/Queries/ReadAllSessionsResponse.cs @@ -0,0 +1,10 @@ +using Shogi.Contracts.Types; +using System.Collections.Generic; + +namespace Shogi.Contracts.Api; + +public class ReadAllSessionsResponse + { + public IList PlayerHasJoinedSessions { get; set; } + public IList AllOtherSessions { get; set; } + } diff --git a/Shogi.Contracts/Api/Queries/ReadSessionResponse.cs b/Shogi.Contracts/Api/Queries/ReadSessionResponse.cs new file mode 100644 index 0000000..f5e9191 --- /dev/null +++ b/Shogi.Contracts/Api/Queries/ReadSessionResponse.cs @@ -0,0 +1,8 @@ +using Shogi.Contracts.Types; + +namespace Shogi.Contracts.Api; + + public class ReadSessionResponse + { + public Session Session { get; set; } + } diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Shogi.Contracts/Shogi.Contracts.csproj similarity index 86% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj rename to Shogi.Contracts/Shogi.Contracts.csproj index 8e671d3..8c5839a 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj +++ b/Shogi.Contracts/Shogi.Contracts.csproj @@ -5,7 +5,7 @@ true 5 enable - True + False Shogi Service Models Contains DTOs use for http requests to Shogi backend services. diff --git a/Shogi.Contracts/Socket/CreateGame.cs b/Shogi.Contracts/Socket/CreateGame.cs new file mode 100644 index 0000000..e447852 --- /dev/null +++ b/Shogi.Contracts/Socket/CreateGame.cs @@ -0,0 +1,13 @@ +using Shogi.Contracts.Types; + +namespace Shogi.Contracts.Socket; + +public class SessionCreatedSocketMessage : ISocketResponse +{ + public SocketAction Action { get; } + + public SessionCreatedSocketMessage() + { + Action = SocketAction.SessionCreated; + } +} diff --git a/Shogi.Contracts/Socket/ISocketRequest.cs b/Shogi.Contracts/Socket/ISocketRequest.cs new file mode 100644 index 0000000..8d5c4db --- /dev/null +++ b/Shogi.Contracts/Socket/ISocketRequest.cs @@ -0,0 +1,9 @@ +using Shogi.Contracts.Types; + +namespace Shogi.Contracts.Socket +{ + public interface ISocketRequest + { + SocketAction Action { get; } + } +} diff --git a/Shogi.Contracts/Socket/ISocketResponse.cs b/Shogi.Contracts/Socket/ISocketResponse.cs new file mode 100644 index 0000000..0e3b95d --- /dev/null +++ b/Shogi.Contracts/Socket/ISocketResponse.cs @@ -0,0 +1,13 @@ +using Shogi.Contracts.Types; + +namespace Shogi.Contracts.Socket; + +public interface ISocketResponse +{ + SocketAction Action { get; } +} + +public class SocketResponse : ISocketResponse +{ + public SocketAction Action { get; set; } +} diff --git a/Shogi.Contracts/Socket/Move.cs b/Shogi.Contracts/Socket/Move.cs new file mode 100644 index 0000000..8dcc72e --- /dev/null +++ b/Shogi.Contracts/Socket/Move.cs @@ -0,0 +1,18 @@ +using Shogi.Contracts.Types; + +namespace Shogi.Contracts.Socket; + +public class MoveResponse : ISocketResponse +{ + public SocketAction Action { get; } + public string SessionName { get; set; } = string.Empty; + /// + /// The player that made the move. + /// + public string PlayerName { get; set; } = string.Empty; + + public MoveResponse() + { + Action = SocketAction.PieceMoved; + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs b/Shogi.Contracts/Types/BoardState.cs similarity index 89% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs rename to Shogi.Contracts/Types/BoardState.cs index 398ba4a..fee2145 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/BoardState.cs +++ b/Shogi.Contracts/Types/BoardState.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +namespace Shogi.Contracts.Types { public class BoardState { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs b/Shogi.Contracts/Types/Move.cs similarity index 85% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs rename to Shogi.Contracts/Types/Move.cs index 7116648..ef72029 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Move.cs +++ b/Shogi.Contracts/Types/Move.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +namespace Shogi.Contracts.Types { public class Move { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs b/Shogi.Contracts/Types/Piece.cs similarity index 73% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs rename to Shogi.Contracts/Types/Piece.cs index 1c0ee78..3c7b335 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Piece.cs +++ b/Shogi.Contracts/Types/Piece.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +namespace Shogi.Contracts.Types { public class Piece { diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Session.cs b/Shogi.Contracts/Types/Session.cs similarity index 61% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Types/Session.cs rename to Shogi.Contracts/Types/Session.cs index a2426c8..d781feb 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/Session.cs +++ b/Shogi.Contracts/Types/Session.cs @@ -1,10 +1,10 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +namespace Shogi.Contracts.Types { public class Session { public string Player1 { get; set; } public string? Player2 { get; set; } - public string GameName { get; set; } + public string SessionName { get; set; } public BoardState BoardState { get; set; } } } diff --git a/Shogi.Contracts/Types/SessionMetadata.cs b/Shogi.Contracts/Types/SessionMetadata.cs new file mode 100644 index 0000000..0a00b2d --- /dev/null +++ b/Shogi.Contracts/Types/SessionMetadata.cs @@ -0,0 +1,8 @@ +namespace Shogi.Contracts.Types +{ + public class SessionMetadata + { + public string Name { get; set; } + public int PlayerCount { get; set; } + } +} diff --git a/Shogi.Contracts/Types/SocketAction.cs b/Shogi.Contracts/Types/SocketAction.cs new file mode 100644 index 0000000..52efc3a --- /dev/null +++ b/Shogi.Contracts/Types/SocketAction.cs @@ -0,0 +1,9 @@ +namespace Shogi.Contracts.Types +{ + public enum SocketAction + { + SessionCreated, + SessionJoined, + PieceMoved + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs b/Shogi.Contracts/Types/User.cs similarity index 68% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs rename to Shogi.Contracts/Types/User.cs index 8e9a72a..610bfa9 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/User.cs +++ b/Shogi.Contracts/Types/User.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +namespace Shogi.Contracts.Types { public class User { diff --git a/Shogi.Contracts/Types/WhichPerspective.cs b/Shogi.Contracts/Types/WhichPerspective.cs new file mode 100644 index 0000000..c29574f --- /dev/null +++ b/Shogi.Contracts/Types/WhichPerspective.cs @@ -0,0 +1,8 @@ +namespace Shogi.Contracts.Types +{ + public enum WhichPlayer + { + Player1, + Player2 + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs b/Shogi.Contracts/Types/WhichPiece.cs similarity index 69% rename from Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs rename to Shogi.Contracts/Types/WhichPiece.cs index 214bdba..6e07e3c 100644 --- a/Gameboard.ShogiUI.Sockets.ServiceModels/Types/WhichPiece.cs +++ b/Shogi.Contracts/Types/WhichPiece.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.ServiceModels.Types +namespace Shogi.Contracts.Types { public enum WhichPiece { diff --git a/Shogi.Database/Post Deployment/Script.PostDeployment.sql b/Shogi.Database/Post Deployment/Script.PostDeployment.sql new file mode 100644 index 0000000..17665e5 --- /dev/null +++ b/Shogi.Database/Post Deployment/Script.PostDeployment.sql @@ -0,0 +1,13 @@ +/* +Post-Deployment Script Template +-------------------------------------------------------------------------------------- + This file contains SQL statements that will be appended to the build script. + Use SQLCMD syntax to include a file in the post-deployment script. + Example: :r .\myfile.sql + Use SQLCMD syntax to reference a variable in the post-deployment script. + Example: :setvar TableName MyTable + SELECT * FROM [$(TableName)] +-------------------------------------------------------------------------------------- +*/ + +:r .\Scripts\PopulateLoginPlatforms.sql \ No newline at end of file diff --git a/Shogi.Database/Post Deployment/Scripts/PopulateLoginPlatforms.sql b/Shogi.Database/Post Deployment/Scripts/PopulateLoginPlatforms.sql new file mode 100644 index 0000000..044b8ba --- /dev/null +++ b/Shogi.Database/Post Deployment/Scripts/PopulateLoginPlatforms.sql @@ -0,0 +1,16 @@ + +DECLARE @LoginPlatforms TABLE ( + [Platform] NVARCHAR(20) +) + +INSERT INTO @LoginPlatforms ([Platform]) +VALUES + ('Guest'), + ('Microsoft'); + +MERGE [user].[LoginPlatform] as t +USING @LoginPlatforms as s +ON t.[Platform] = s.[Platform] +WHEN NOT MATCHED THEN + INSERT ([Platform]) + VALUES (s.[Platform]); \ No newline at end of file diff --git a/Shogi.Database/Session/Session.sql b/Shogi.Database/Session/Session.sql new file mode 100644 index 0000000..592c0a1 --- /dev/null +++ b/Shogi.Database/Session/Session.sql @@ -0,0 +1 @@ +CREATE SCHEMA [session] diff --git a/Shogi.Database/Session/Stored Procedures/CreateBoardState.sql b/Shogi.Database/Session/Stored Procedures/CreateBoardState.sql new file mode 100644 index 0000000..c906a33 --- /dev/null +++ b/Shogi.Database/Session/Stored Procedures/CreateBoardState.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[CreateBoardState] + @boardStateDocument NVARCHAR(max), + @sessionName NVARCHAR(100) +AS +BEGIN + +SET NOCOUNT ON + +INSERT INTO [session].[BoardState] (Document, SessionId) + SELECT + @boardStateDocument, + Id + FROM [session].[Session] + WHERE [Name] = @sessionName; + +END \ No newline at end of file diff --git a/Shogi.Database/Session/Stored Procedures/CreateSession.sql b/Shogi.Database/Session/Stored Procedures/CreateSession.sql new file mode 100644 index 0000000..8d68ac2 --- /dev/null +++ b/Shogi.Database/Session/Stored Procedures/CreateSession.sql @@ -0,0 +1,24 @@ +CREATE PROCEDURE [session].[CreateSession] + @SessionName [session].[SessionName], + @Player1Name [user].[UserName], + @InitialBoardStateDocument [session].[JsonDocument] +AS +BEGIN + +SET NOCOUNT ON +SET XACT_ABORT ON + +BEGIN TRANSACTION + INSERT INTO [session].[Session] ([Name], Player1Id) + SELECT + @SessionName, + Id + FROM [user].[User] + WHERE [Name] = @Player1Name; + + INSERT INTO [session].[BoardState] (Document, SessionId) + VALUES + (@InitialBoardStateDocument, SCOPE_IDENTITY()); +COMMIT + +END \ No newline at end of file diff --git a/Shogi.Database/Session/Stored Procedures/ReadAllSessionsMetadata.sql b/Shogi.Database/Session/Stored Procedures/ReadAllSessionsMetadata.sql new file mode 100644 index 0000000..1ffacac --- /dev/null +++ b/Shogi.Database/Session/Stored Procedures/ReadAllSessionsMetadata.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [session].[ReadAllSessionsMetadata] +AS + +SET NOCOUNT ON; + +SELECT + [Name], + CASE Player2Id + WHEN NULL THEN 1 + ELSE 2 + END AS PlayerCount +FROM [session].[Session]; diff --git a/Shogi.Database/Session/Stored Procedures/UpdateSession.sql b/Shogi.Database/Session/Stored Procedures/UpdateSession.sql new file mode 100644 index 0000000..0575e4e --- /dev/null +++ b/Shogi.Database/Session/Stored Procedures/UpdateSession.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[UpdateSession] + @SessionName [session].[SessionName], + @BoardStateJson [session].[JsonDocument] +AS +BEGIN + SET NOCOUNT ON; + + UPDATE bs + SET bs.Document = @BoardStateJson + FROM [session].[BoardState] bs + INNER JOIN [session].[Session] s on s.Id = bs.SessionId + WHERE s.Name = @SessionName; + +END diff --git a/Shogi.Database/Session/Tables/BoardState.sql b/Shogi.Database/Session/Tables/BoardState.sql new file mode 100644 index 0000000..e55d176 --- /dev/null +++ b/Shogi.Database/Session/Tables/BoardState.sql @@ -0,0 +1,10 @@ +CREATE TABLE [session].[BoardState] +( + [Id] BIGINT NOT NULL PRIMARY KEY IDENTITY, + [Document] NVARCHAR(max) NOT NULL, + [SessionId] BIGINT NOT NULL, + + CONSTRAINT [Document must be json] CHECK (isjson(Document)=1), + CONSTRAINT FK_BoardState_Session FOREIGN KEY (SessionId) + REFERENCES [session].[Session] (Id) ON DELETE CASCADE ON UPDATE CASCADE +) diff --git a/Shogi.Database/Session/Tables/Session.sql b/Shogi.Database/Session/Tables/Session.sql new file mode 100644 index 0000000..f570c64 --- /dev/null +++ b/Shogi.Database/Session/Tables/Session.sql @@ -0,0 +1,16 @@ +CREATE TABLE [session].[Session] +( + Id BIGINT NOT NULL PRIMARY KEY IDENTITY, + [Name] [session].[SessionName] NOT NULL UNIQUE, + Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(), + GameOver BIT NOT NULL DEFAULT 0, + Player1Id BIGINT NOT NULL, + Player2Id BIGINT NULL, + + CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id) + ON DELETE NO ACTION + ON UPDATE NO ACTION +) diff --git a/Shogi.Database/Session/Types/JsonDocument.sql b/Shogi.Database/Session/Types/JsonDocument.sql new file mode 100644 index 0000000..212ae06 --- /dev/null +++ b/Shogi.Database/Session/Types/JsonDocument.sql @@ -0,0 +1,2 @@ +CREATE TYPE [session].[JsonDocument] + FROM NVARCHAR(max) NOT NULL diff --git a/Shogi.Database/Session/Types/SessionName.sql b/Shogi.Database/Session/Types/SessionName.sql new file mode 100644 index 0000000..0e6f505 --- /dev/null +++ b/Shogi.Database/Session/Types/SessionName.sql @@ -0,0 +1,2 @@ +CREATE TYPE [session].[SessionName] + FROM nvarchar(50) NOT NULL diff --git a/Shogi.Database/Shogi.Database.sqlproj b/Shogi.Database/Shogi.Database.sqlproj new file mode 100644 index 0000000..ea8b694 --- /dev/null +++ b/Shogi.Database/Shogi.Database.sqlproj @@ -0,0 +1,91 @@ + + + + Debug + AnyCPU + Shogi.Database + 2.0 + 4.1 + {9b115b71-088f-41ef-858f-c7b155271a9f} + Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider + Database + + + Shogi.Database + Shogi.Database + 1033, CI + BySchemaAndSchemaType + True + v4.7.2 + CS + Properties + False + True + True + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + false + true + full + false + true + true + prompt + 4 + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Shogi.Database/User/StoredProcedures/CreateUser.sql b/Shogi.Database/User/StoredProcedures/CreateUser.sql new file mode 100644 index 0000000..d86ac22 --- /dev/null +++ b/Shogi.Database/User/StoredProcedures/CreateUser.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [user].[CreateUser] + @Name [user].[UserName], + @DisplayName NVARCHAR(100), + @Platform NVARCHAR(20) +AS +BEGIN + +SET NOCOUNT ON + +INSERT INTO [user].[User] ([Name], DisplayName, [Platform]) +VALUES + (@Name, @DisplayName, @Platform); + +END \ No newline at end of file diff --git a/Shogi.Database/User/StoredProcedures/ReadUser.sql b/Shogi.Database/User/StoredProcedures/ReadUser.sql new file mode 100644 index 0000000..13e0a10 --- /dev/null +++ b/Shogi.Database/User/StoredProcedures/ReadUser.sql @@ -0,0 +1,11 @@ +CREATE PROCEDURE [user].[ReadUser] + @Name [user].[UserName] +AS +BEGIN + SELECT + [Name] as Id, + DisplayName, + [Platform] + FROM [user].[User] + WHERE [Name] = @Name; +END \ No newline at end of file diff --git a/Shogi.Database/User/Tables/LoginPlatform.sql b/Shogi.Database/User/Tables/LoginPlatform.sql new file mode 100644 index 0000000..e63d0ca --- /dev/null +++ b/Shogi.Database/User/Tables/LoginPlatform.sql @@ -0,0 +1,4 @@ +CREATE TABLE [user].[LoginPlatform] +( + [Platform] NVARCHAR(20) NOT NULL PRIMARY KEY +) diff --git a/Shogi.Database/User/Tables/User.sql b/Shogi.Database/User/Tables/User.sql new file mode 100644 index 0000000..82ec654 --- /dev/null +++ b/Shogi.Database/User/Tables/User.sql @@ -0,0 +1,11 @@ +CREATE TABLE [user].[User] +( + [Id] BIGINT NOT NULL PRIMARY KEY IDENTITY, + [Name] [user].[UserName] NOT NULL UNIQUE, + [DisplayName] NVARCHAR(100) NOT NULL, + [Platform] NVARCHAR(20) NOT NULL, + + CONSTRAINT User_Platform FOREIGN KEY ([Platform]) References [user].[LoginPlatform] ([Platform]) + ON DELETE CASCADE + ON UPDATE CASCADE +) diff --git a/Shogi.Database/User/Types/UserName.sql b/Shogi.Database/User/Types/UserName.sql new file mode 100644 index 0000000..224d537 --- /dev/null +++ b/Shogi.Database/User/Types/UserName.sql @@ -0,0 +1,2 @@ +CREATE TYPE [user].[UserName] + FROM nvarchar(100) NOT NULL diff --git a/Shogi.Database/User/User.sql b/Shogi.Database/User/User.sql new file mode 100644 index 0000000..08baf83 --- /dev/null +++ b/Shogi.Database/User/User.sql @@ -0,0 +1 @@ +CREATE SCHEMA [user] diff --git a/Shogi.Domain.UnitTests/RookShould.cs b/Shogi.Domain.UnitTests/RookShould.cs deleted file mode 100644 index 0a2bcf4..0000000 --- a/Shogi.Domain.UnitTests/RookShould.cs +++ /dev/null @@ -1,227 +0,0 @@ -using FluentAssertions; -using Shogi.Domain.Pathing; -using Shogi.Domain.Pieces; -using System.Numerics; -using Xunit; - -namespace Shogi.Domain.UnitTests -{ - public class RookShould - { - public class MoveSet - { - private readonly Rook rook1; - private readonly Rook rook2; - - public MoveSet() - { - this.rook1 = new Rook(WhichPlayer.Player1); - this.rook2 = new Rook(WhichPlayer.Player2); - } - - [Fact] - public void Player1_HasCorrectMoveSet() - { - var moveSet = rook1.MoveSet; - moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - } - - [Fact] - public void Player1_Promoted_HasCorrectMoveSet() - { - // Arrange - rook1.Promote(); - rook1.IsPromoted.Should().BeTrue(); - - // Assert - var moveSet = rook1.MoveSet; - moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); - } - - [Fact] - public void Player2_HasCorrectMoveSet() - { - var moveSet = rook2.MoveSet; - moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - } - - [Fact] - public void Player2_Promoted_HasCorrectMoveSet() - { - // Arrange - rook2.Promote(); - rook2.IsPromoted.Should().BeTrue(); - - // Assert - var moveSet = rook2.MoveSet; - moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); - } - - - } - private readonly Rook rookPlayer1; - private readonly Rook rookPlayer2; - - public RookShould() - { - this.rookPlayer1 = new Rook(WhichPlayer.Player1); - this.rookPlayer2 = new Rook(WhichPlayer.Player2); - } - - [Fact] - public void Promote() - { - this.rookPlayer1.IsPromoted.Should().BeFalse(); - this.rookPlayer1.CanPromote.Should().BeTrue(); - this.rookPlayer1.Promote(); - this.rookPlayer1.IsPromoted.Should().BeTrue(); - this.rookPlayer1.CanPromote.Should().BeFalse(); - } - - [Fact] - public void GetStepsFromStartToEnd_Player1NotPromoted_LateralMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); - - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); - } - - [Fact] - public void GetStepsFromStartToEnd_Player1NotPromoted_DiagonalMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); - - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().BeEmpty(); - } - - [Fact] - public void GetStepsFromStartToEnd_Player1Promoted_LateralMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); - rookPlayer1.Promote(); - - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); - } - - [Fact] - public void GetStepsFromStartToEnd_Player1Promoted_DiagonalMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); - rookPlayer1.Promote(); - - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(1); - steps.Should().Contain(new Vector2(1, 1)); - } - - [Fact] - public void GetStepsFromStartToEnd_Player2NotPromoted_LateralMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); - - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); - } - - [Fact] - public void GetStepsFromStartToEnd_Player2NotPromoted_DiagonalMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); - - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - - rookPlayer1.IsPromoted.Should().BeFalse(); - steps.Should().BeEmpty(); - } - - [Fact] - public void GetStepsFromStartToEnd_Player2Promoted_LateralMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(0, 5); - rookPlayer1.Promote(); - - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(5); - steps.Should().Contain(new Vector2(0, 1)); - steps.Should().Contain(new Vector2(0, 2)); - steps.Should().Contain(new Vector2(0, 3)); - steps.Should().Contain(new Vector2(0, 4)); - steps.Should().Contain(new Vector2(0, 5)); - } - - [Fact] - public void GetStepsFromStartToEnd_Player2Promoted_DiagonalMove() - { - Vector2 start = new(0, 0); - Vector2 end = new(1, 1); - rookPlayer1.Promote(); - - var steps = rookPlayer1.GetPathFromStartToEnd(start, end); - - rookPlayer1.IsPromoted.Should().BeTrue(); - steps.Should().HaveCount(1); - steps.Should().Contain(new Vector2(1, 1)); - } - } -} diff --git a/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj b/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj deleted file mode 100644 index 76e3c8e..0000000 --- a/Shogi.Domain.UnitTests/Shogi.Domain.UnitTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net6.0 - enable - - false - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/Shogi.Domain.UnitTests/ShogiShould.cs b/Shogi.Domain.UnitTests/ShogiShould.cs deleted file mode 100644 index e36b046..0000000 --- a/Shogi.Domain.UnitTests/ShogiShould.cs +++ /dev/null @@ -1,463 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Execution; -using System; -using Xunit; -using Xunit.Abstractions; - -namespace Shogi.Domain.UnitTests -{ - public class ShogiShould - { - private readonly ITestOutputHelper console; - public ShogiShould(ITestOutputHelper console) - { - this.console = console; - } - - [Fact] - public void MoveAPieceToAnEmptyPosition() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - board["A4"].Should().BeNull(); - var expectedPiece = board["A3"]; - expectedPiece.Should().NotBeNull(); - - // Act - shogi.Move("A3", "A4", false); - - // Assert - board["A3"].Should().BeNull(); - board["A4"].Should().Be(expectedPiece); - } - - [Fact] - public void AllowValidMoves_AfterCheck() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // P1 Bishop puts P2 in check - shogi.Move("B2", "G7", false); - board.InCheck.Should().Be(WhichPlayer.Player2); - - // Act - P2 is able to un-check theirself. - /// P2 King moves out of check - shogi.Move("E9", "E8", false); - - // Assert - using (new AssertionScope()) - { - board.InCheck.Should().BeNull(); - } - } - - [Fact] - public void PreventInvalidMoves_MoveFromEmptyPosition() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - board["D5"].Should().BeNull(); - - // Act - var act = () => shogi.Move("D5", "D6", false); - - // Assert - act.Should().Throw(); - board["D5"].Should().BeNull(); - board["D6"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - - [Fact] - public void PreventInvalidMoves_MoveToCurrentPosition() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - var expectedPiece = board["A3"]; - - // Act - P1 "moves" pawn to the position it already exists at. - var act = () => shogi.Move("A3", "A3", false); - - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A3"].Should().Be(expectedPiece); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } - - [Fact] - public void PreventInvalidMoves_MoveSet() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - var expectedPiece = board["A1"]; - expectedPiece!.WhichPiece.Should().Be(WhichPiece.Lance); - - // Act - Move Lance illegally - var act = () => shogi.Move("A1", "D5", false); - - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A1"].Should().Be(expectedPiece); - board["A5"].Should().BeNull(); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } - - [Fact] - public void PreventInvalidMoves_Ownership() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - var expectedPiece = board["A7"]; - expectedPiece!.Owner.Should().Be(WhichPlayer.Player2); - board.WhoseTurn.Should().Be(WhichPlayer.Player1); - - // Act - Move Player2 Pawn when it is Player1 turn. - var act = () => shogi.Move("A7", "A6", false); - - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A7"].Should().Be(expectedPiece); - board["A6"].Should().BeNull(); - } - } - - [Fact] - public void PreventInvalidMoves_MoveThroughAllies() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - var lance = board["A1"]; - var pawn = board["A3"]; - lance!.Owner.Should().Be(pawn!.Owner); - - // Act - Move P1 Lance through P1 Pawn. - var act = () => shogi.Move("A1", "A5", false); - - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board["A1"].Should().Be(lance); - board["A3"].Should().Be(pawn); - board["A5"].Should().BeNull(); - } - } - - [Fact] - public void PreventInvalidMoves_CaptureAlly() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - var knight = board["B1"]; - var pawn = board["C3"]; - knight!.Owner.Should().Be(pawn!.Owner); - - // Act - P1 Knight tries to capture P1 Pawn. - var act = () => shogi.Move("B1", "C3", false); - - // Arrange - using (new AssertionScope()) - { - act.Should().Throw(); - board["B1"].Should().Be(knight); - board["C3"].Should().Be(pawn); - board.Player1Hand.Should().BeEmpty(); - board.Player2Hand.Should().BeEmpty(); - } - } - - [Fact] - public void PreventInvalidMoves_Check() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - // P1 Bishop puts P2 in check - shogi.Move("B2", "G7", false); - board.InCheck.Should().Be(WhichPlayer.Player2); - var lance = board["I9"]; - - // Act - P2 moves Lance while in check. - var act = () => shogi.Move("I9", "I8", false); - - // Assert - using (new AssertionScope()) - { - act.Should().Throw(); - board.InCheck.Should().Be(WhichPlayer.Player2); - board["I9"].Should().Be(lance); - board["I8"].Should().BeNull(); - } - } - - [Fact] - // TODO: Consider nesting classes to share this setup in a constructor but have act and assert as separate facts. - public void PreventInvalidDrops_MoveSet() - { - // Arrange - var board = new BoardState(); - var shogi = new Session(board); - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("I7", "I6", false); - // P1 Bishop takes P2 Pawn. - shogi.Move("B2", "G7", false); - // P2 Gold, block check from P1 Bishop. - shogi.Move("F9", "F8", false); - // P1 Bishop takes P2 Bishop, promotes so it can capture P2 Knight and P2 Lance - shogi.Move("G7", "H8", true); - // P2 Pawn again - shogi.Move("I6", "I5", false); - // P1 Bishop takes P2 Knight - shogi.Move("H8", "H9", false); - // P2 Pawn again - shogi.Move("I5", "I4", false); - // P1 Bishop takes P2 Lance - shogi.Move("H9", "I9", false); - // P2 Pawn captures P1 Pawn - shogi.Move("I4", "I3", false); - board.Player1Hand.Count.Should().Be(4); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - board.WhoseTurn.Should().Be(WhichPlayer.Player1); - - // Act | Assert - Illegally placing Knight from the hand in farthest rank. - board["H9"].Should().BeNull(); - var act = () => shogi.Move(WhichPiece.Knight, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - - // Act | Assert - Illegally placing Knight from the hand in second farthest row. - board["H8"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Knight, "H8"); - act.Should().Throw(); - board["H8"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); - - // Act | Assert - Illegally place Lance from the hand. - board["H9"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Knight, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); - - // Act | Assert - Illegally place Pawn from the hand. - board["H9"].Should().BeNull(); - act = () => shogi.Move(WhichPiece.Pawn, "H9"); - act.Should().Throw(); - board["H9"].Should().BeNull(); - board.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); - - // // Act | Assert - Illegally place Pawn from the hand in a row which already has an unpromoted Pawn. - // // TODO - } - - //[Fact] - //public void PreventInvalidDrop_Check() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Pawn, arbitrary move. - // new Move("A3", "A4"), - // // P2 Bishop takes P1 Bishop - // new Move("H8", "B2"), - // // P1 Silver takes P2 Bishop - // new Move("C1", "B2"), - // // P2 Pawn, arbtrary move - // new Move("A7", "A6"), - // // P1 drop Bishop, place P2 in check - // new Move(WhichPiece.Bishop, "G7") - // }; - // var shogi = new Shogi(moves); - // shogi.InCheck.Should().Be(WhichPlayer.Player2); - // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // boardState["E5"].Should().BeNull(); - - // // Act - P2 places a Bishop while in check. - // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "E5")); - - // // Assert - // dropSuccess.Should().BeFalse(); - // boardState["E5"].Should().BeNull(); - // shogi.InCheck.Should().Be(WhichPlayer.Player2); - // shogi.Player2Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - //} - - //[Fact] - //public void PreventInvalidDrop_Capture() - //{ - // // Arrange - // var moves = new[] - // { - // // P1 Pawn - // new Move("C3", "C4"), - // // P2 Pawn - // new Move("G7", "G6"), - // // P1 Bishop capture P2 Bishop - // new Move("B2", "H8"), - // // P2 Pawn - // new Move("G6", "G5") - // }; - // var shogi = new Shogi(moves); - // using (new AssertionScope()) - // { - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // boardState["I9"].Should().NotBeNull(); - // boardState["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - // boardState["I9"].Owner.Should().Be(WhichPlayer.Player2); - // } - - // // Act - P1 tries to place a piece where an opponent's piece resides. - // var dropSuccess = shogi.Move(new Move(WhichPiece.Bishop, "I9")); - - // // Assert - // using (new AssertionScope()) - // { - // dropSuccess.Should().BeFalse(); - // shogi.Player1Hand.Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop); - // boardState["I9"].Should().NotBeNull(); - // boardState["I9"].WhichPiece.Should().Be(WhichPiece.Lance); - // boardState["I9"].Owner.Should().Be(WhichPlayer.Player2); - // } - //} - - [Fact] - public void Check() - { - // Arrange - var boardState = new BoardState(); - var shogi = new Session(boardState); - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - - // Act - P1 Bishop, check - shogi.Move("B2", "G7", false); - - // Assert - boardState.InCheck.Should().Be(WhichPlayer.Player2); - } - - [Fact] - public void Promote() - { - // Arrange - var boardState = new BoardState(); - var shogi = new Session(boardState); - // P1 Pawn - shogi.Move("C3", "C4", false); - // P2 Pawn - shogi.Move("G7", "G6", false); - - // Act - P1 moves across promote threshold. - shogi.Move("B2", "G7", true); - - // Assert - using (new AssertionScope()) - { - boardState["B2"].Should().BeNull(); - boardState["G7"].Should().NotBeNull(); - boardState["G7"]!.WhichPiece.Should().Be(WhichPiece.Bishop); - boardState["G7"]!.Owner.Should().Be(WhichPlayer.Player1); - boardState["G7"]!.IsPromoted.Should().BeTrue(); - } - } - - [Fact] - public void Capture() - { - // Arrange - var boardState = new BoardState(); - var shogi = new Session(boardState); - var p1Bishop = boardState["B2"]; - p1Bishop!.WhichPiece.Should().Be(WhichPiece.Bishop); - shogi.Move("C3", "C4", false); - shogi.Move("G7", "G6", false); - - // Act - P1 Bishop captures P2 Bishop - shogi.Move("B2", "H8", false); - - // Assert - boardState["B2"].Should().BeNull(); - boardState["H8"].Should().Be(p1Bishop); - - boardState - .Player1Hand - .Should() - .ContainSingle(p => p.WhichPiece == WhichPiece.Bishop && p.Owner == WhichPlayer.Player1); - } - - [Fact] - public void CheckMate() - { - // Arrange - var boardState = new BoardState(); - var shogi = new Session(boardState); - // P1 Rook - shogi.Move("H2", "E2", false); - // P2 Gold - shogi.Move("F9", "G8", false); - // P1 Pawn - shogi.Move("E3", "E4", false); - // P2 other Gold - shogi.Move("D9", "C8", false); - // P1 same Pawn - shogi.Move("E4", "E5", false); - // P2 Pawn - shogi.Move("E7", "E6", false); - // P1 Pawn takes P2 Pawn - shogi.Move("E5", "E6", false); - // P2 King - shogi.Move("E9", "E8", false); - // P1 Pawn promotes; threatens P2 King - shogi.Move("E6", "E7", true); - // P2 King retreat - shogi.Move("E8", "E9", false); - - // Act - P1 Pawn wins by checkmate. - shogi.Move("E7", "E8", false); - - // Assert - checkmate - console.WriteLine(shogi.ToStringStateAsAscii()); - boardState.IsCheckmate.Should().BeTrue(); - boardState.InCheck.Should().Be(WhichPlayer.Player2); - } - } -} diff --git a/Shogi.Domain/Aggregates/Session.cs b/Shogi.Domain/Aggregates/Session.cs new file mode 100644 index 0000000..5bd0afc --- /dev/null +++ b/Shogi.Domain/Aggregates/Session.cs @@ -0,0 +1,242 @@ +using Shogi.Domain.ValueObjects; +using System.Text; + +namespace Shogi.Domain; + +/// +/// Facilitates Shogi board state transitions, cognisant of Shogi rules. +/// The board is always from Player1's perspective. +/// [0,0] is the lower-left position, [8,8] is the higher-right position +/// +public sealed class Session +{ + private readonly StandardRules rules; + + public Session(string name, BoardState initialState, string player1, string? player2 = null) + { + Name = name; + Player1 = player1; + Player2 = player2; + BoardState = initialState; + rules = new StandardRules(BoardState); + } + + public BoardState BoardState { get; } + public string Name { get; } + public string Player1 { get; } + public string? Player2 { get; } + + /// + /// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game. + /// + /// + /// The strategy involves simulating a move on a throw-away board state that can be used to + /// validate legal vs illegal moves without having to worry about reverting board state. + /// + /// + public void Move(string from, string to, bool isPromotion) + { + var simulationState = new BoardState(BoardState); + var simulation = new StandardRules(simulationState); + var moveResult = simulation.Move(from, to, isPromotion); + if (!moveResult.Success) + { + throw new InvalidOperationException(moveResult.Reason); + } + + // If already in check, assert the move that resulted in check no longer results in check. + if (BoardState.InCheck == BoardState.WhoseTurn + && simulation.IsOpposingKingThreatenedByPosition(BoardState.PreviousMoveTo)) + { + throw new InvalidOperationException("Unable to move because you are still in check."); + } + + var otherPlayer = BoardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + if (simulation.IsPlayerInCheckAfterMove()) + { + throw new InvalidOperationException("Illegal move. This move places you in check."); + } + + _ = rules.Move(from, to, isPromotion); + if (rules.IsOpponentInCheckAfterMove()) + { + BoardState.InCheck = otherPlayer; + if (rules.IsOpponentInCheckMate()) + { + BoardState.IsCheckmate = true; + } + } + else + { + BoardState.InCheck = null; + } + BoardState.WhoseTurn = otherPlayer; + } + + public void Move(WhichPiece pieceInHand, string to) + { + var index = BoardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); + if (index == -1) + { + throw new InvalidOperationException($"{pieceInHand} does not exist in the hand."); + } + + if (BoardState[to] != null) + { + throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); + } + + var toVector = Notation.FromBoardNotation(to); + switch (pieceInHand) + { + case WhichPiece.Knight: + { + // Knight cannot be placed onto the farthest two ranks from the hand. + if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6 + || BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2) + { + throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement."); + } + break; + } + case WhichPiece.Lance: + case WhichPiece.Pawn: + { + // Lance and Pawn cannot be placed onto the farthest rank from the hand. + if (BoardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8 + || BoardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0) + { + throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement."); + } + break; + } + } + + var tempBoard = new BoardState(BoardState); + var simulation = new StandardRules(tempBoard); + var moveResult = simulation.Move(pieceInHand, to); + if (!moveResult.Success) + { + throw new InvalidOperationException(moveResult.Reason); + } + + var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; + if (BoardState.InCheck == BoardState.WhoseTurn) + { + //if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn)) + //{ + // throw new InvalidOperationException("Illegal move. You're still in check!"); + //} + } + + var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition; + //if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer)) + //{ + + //} + + //rules.Move(from, to, isPromotion); + //if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer)) + //{ + // board.InCheck = otherPlayer; + // board.IsCheckmate = rules.EvaluateCheckmate(); + //} + //else + //{ + // board.InCheck = null; + //} + BoardState.WhoseTurn = otherPlayer; + } + + /// + /// Prints a ASCII representation of the board for debugging board state. + /// + /// + public string ToStringStateAsAscii() + { + var builder = new StringBuilder(); + builder.Append(" "); + builder.Append("Player 2(.)"); + builder.AppendLine(); + for (var rank = 8; rank >= 0; rank--) + { + // Horizontal line + builder.Append(" - "); + for (var file = 0; file < 8; file++) builder.Append("- - "); + builder.Append("- -"); + + // Print Rank ruler. + builder.AppendLine(); + builder.Append($"{rank + 1} "); + + // Print pieces. + builder.Append(" |"); + for (var x = 0; x < 9; x++) + { + var piece = BoardState[x, rank]; + if (piece == null) + { + builder.Append(" "); + } + else + { + builder.AppendFormat("{0}", ToAscii(piece)); + } + builder.Append('|'); + } + builder.AppendLine(); + } + + // Horizontal line + builder.Append(" - "); + for (var x = 0; x < 8; x++) builder.Append("- - "); + builder.Append("- -"); + builder.AppendLine(); + builder.Append(" "); + builder.Append("Player 1"); + + builder.AppendLine(); + builder.AppendLine(); + // Print File ruler. + builder.Append(" "); + builder.Append(" A B C D E F G H I "); + + return builder.ToString(); + } + + /// + /// + /// + /// + /// + /// A string with three characters. + /// The first character indicates promotion status. + /// The second character indicates piece. + /// The third character indicates ownership. + /// + private static string ToAscii(Piece piece) + { + var builder = new StringBuilder(); + if (piece.IsPromoted) builder.Append('^'); + else builder.Append(' '); + + var name = piece.WhichPiece switch + { + WhichPiece.King => "K", + WhichPiece.GoldGeneral => "G", + WhichPiece.SilverGeneral => "S", + WhichPiece.Bishop => "B", + WhichPiece.Rook => "R", + WhichPiece.Knight => "k", + WhichPiece.Lance => "L", + WhichPiece.Pawn => "P", + _ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."), + }; + builder.Append(name); + + if (piece.Owner == WhichPlayer.Player2) builder.Append('.'); + else builder.Append(' '); + + return builder.ToString(); + } +} diff --git a/Shogi.Domain/BoardState.cs b/Shogi.Domain/BoardState.cs index 9802010..fb50200 100644 --- a/Shogi.Domain/BoardState.cs +++ b/Shogi.Domain/BoardState.cs @@ -1,246 +1,251 @@ -using Shogi.Domain.Pieces; -using BoardTile = System.Collections.Generic.KeyValuePair; +using Shogi.Domain.ValueObjects; +using BoardTile = System.Collections.Generic.KeyValuePair; namespace Shogi.Domain { - public class BoardState - { - public delegate void ForEachDelegate(Piece element, Vector2 position); - /// - /// Key is position notation, such as "E4". - /// - private readonly Dictionary board; + public class BoardState + { + /// + /// Board state before any moves have been made, using standard setup and rules. + /// + public static readonly BoardState StandardStarting = new(); + + public delegate void ForEachDelegate(Piece element, Vector2 position); + /// + /// Key is position notation, such as "E4". + /// + private readonly Dictionary board; - public BoardState(Dictionary state) - { - board = state; - Player1Hand = new List(); - Player2Hand = new List(); - PreviousMoveTo = Vector2.Zero; - } + public BoardState(Dictionary state) + { + board = state; + Player1Hand = new List(); + Player2Hand = new List(); + PreviousMoveTo = Vector2.Zero; + } - public BoardState() - { - board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); - InitializeBoardState(); - Player1Hand = new List(); - Player2Hand = new List(); - PreviousMoveTo = Vector2.Zero; - } + public BoardState() + { + board = new Dictionary(81, StringComparer.OrdinalIgnoreCase); + InitializeBoardState(); + Player1Hand = new List(); + Player2Hand = new List(); + PreviousMoveTo = Vector2.Zero; + } - public Dictionary State => board; - public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; - public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => - { - var piece = kvp.Value; - return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1; - }).Key); - public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => - { - var piece = kvp.Value; - return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2; - }).Key); - public List Player1Hand { get; } - public List Player2Hand { get; } - public Vector2 PreviousMoveFrom { get; private set; } - public Vector2 PreviousMoveTo { get; private set; } - public WhichPlayer WhoseTurn { get; set; } - public WhichPlayer? InCheck { get; set; } - public bool IsCheckmate { get; set; } + public Dictionary State => board; + public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; + public Vector2 Player1KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => + { + var piece = kvp.Value; + return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1; + }).Key); + public Vector2 Player2KingPosition => Notation.FromBoardNotation(this.board.Where(kvp => kvp.Value != null).Single(kvp => + { + var piece = kvp.Value; + return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2; + }).Key); + public List Player1Hand { get; } + public List Player2Hand { get; } + public Vector2 PreviousMoveFrom { get; private set; } + public Vector2 PreviousMoveTo { get; private set; } + public WhichPlayer WhoseTurn { get; set; } + public WhichPlayer? InCheck { get; set; } + public bool IsCheckmate { get; set; } - /// - /// Copy constructor. - /// - public BoardState(BoardState other) : this() - { - foreach (var kvp in other.board) - { - // Replace copy constructor with static factory method in Piece.cs - board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value); - } - WhoseTurn = other.WhoseTurn; - InCheck = other.InCheck; - IsCheckmate = other.IsCheckmate; - PreviousMoveTo = other.PreviousMoveTo; - Player1Hand.AddRange(other.Player1Hand); - Player2Hand.AddRange(other.Player2Hand); - } + /// + /// Copy constructor. + /// + public BoardState(BoardState other) : this() + { + foreach (var kvp in other.board) + { + // Replace copy constructor with static factory method in Piece.cs + board[kvp.Key] = kvp.Value == null ? null : Piece.CreateCopy(kvp.Value); + } + WhoseTurn = other.WhoseTurn; + InCheck = other.InCheck; + IsCheckmate = other.IsCheckmate; + PreviousMoveTo = other.PreviousMoveTo; + Player1Hand.AddRange(other.Player1Hand); + Player2Hand.AddRange(other.Player2Hand); + } - public Piece? this[string notation] - { - // TODO: Validate "notation" here and throw an exception if invalid. - get => board[notation]; - set => board[notation] = value; - } + public Piece? this[string notation] + { + // TODO: Validate "notation" here and throw an exception if invalid. + get => board[notation]; + set => board[notation] = value; + } - public Piece? this[Vector2 vector] - { - get => this[Notation.ToBoardNotation(vector)]; - set => this[Notation.ToBoardNotation(vector)] = value; - } + public Piece? this[Vector2 vector] + { + get => this[Notation.ToBoardNotation(vector)]; + set => this[Notation.ToBoardNotation(vector)] = value; + } - public Piece? this[int x, int y] - { - get => this[Notation.ToBoardNotation(x, y)]; - set => this[Notation.ToBoardNotation(x, y)] = value; - } + public Piece? this[int x, int y] + { + get => this[Notation.ToBoardNotation(x, y)]; + set => this[Notation.ToBoardNotation(x, y)] = value; + } - internal void RememberAsMostRecentMove(Vector2 from, Vector2 to) - { - PreviousMoveFrom = from; - PreviousMoveTo = to; - } + internal void RememberAsMostRecentMove(Vector2 from, Vector2 to) + { + PreviousMoveFrom = from; + PreviousMoveTo = to; + } - /// - /// Returns true if the given path can be traversed without colliding into a piece. - /// - public bool IsPathBlocked(IEnumerable path) - { - return !path.Any() - || path.SkipLast(1).Any(position => this[position] != null) - || this[path.Last()]?.Owner == WhoseTurn; - } + /// + /// Returns true if the given path can be traversed without colliding into a piece. + /// + public bool IsPathBlocked(IEnumerable path) + { + return !path.Any() + || path.SkipLast(1).Any(position => this[position] != null) + || this[path.Last()]?.Owner == WhoseTurn; + } - internal bool IsWithinPromotionZone(Vector2 position) - { - return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5) - || (WhoseTurn == WhichPlayer.Player2 && position.Y < 3); - } + internal bool IsWithinPromotionZone(Vector2 position) + { + return (WhoseTurn == WhichPlayer.Player1 && position.Y > 5) + || (WhoseTurn == WhichPlayer.Player2 && position.Y < 3); + } - internal static bool IsWithinBoardBoundary(Vector2 position) - { - return position.X <= 8 && position.X >= 0 - && position.Y <= 8 && position.Y >= 0; - } + internal static bool IsWithinBoardBoundary(Vector2 position) + { + return position.X <= 8 && position.X >= 0 + && position.Y <= 8 && position.Y >= 0; + } - internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board - .Where(kvp => kvp.Value?.Owner == whichPlayer) - .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) - .ToList(); + internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board + .Where(kvp => kvp.Value?.Owner == whichPlayer) + .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) + .ToList(); - internal void Capture(Vector2 to) - { - var piece = this[to]; - if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); + internal void Capture(Vector2 to) + { + var piece = this[to]; + if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); - piece.Capture(WhoseTurn); - ActivePlayerHand.Add(piece); - } + piece.Capture(WhoseTurn); + ActivePlayerHand.Add(piece); + } - /// - /// Does not include the start position. - /// - internal static IEnumerable GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction) - { - var next = start; - while (IsWithinBoardBoundary(next + direction)) - { - next += direction; - yield return next; - } - } + /// + /// Does not include the start position. + /// + internal static IEnumerable GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction) + { + var next = start; + while (IsWithinBoardBoundary(next + direction)) + { + next += direction; + yield return next; + } + } - internal Piece? QueryFirstPieceInPath(IEnumerable path) - { - foreach (var step in path) - { - if (this[step] != null) return this[step]; - } - return null; - } + internal Piece? QueryFirstPieceInPath(IEnumerable path) + { + foreach (var step in path) + { + if (this[step] != null) return this[step]; + } + return null; + } - private void InitializeBoardState() - { - this["A1"] = new Lance(WhichPlayer.Player1); - this["B1"] = new Knight(WhichPlayer.Player1); - this["C1"] = new SilverGeneral(WhichPlayer.Player1); - this["D1"] = new GoldGeneral(WhichPlayer.Player1); - this["E1"] = new King(WhichPlayer.Player1); - this["F1"] = new GoldGeneral(WhichPlayer.Player1); - this["G1"] = new SilverGeneral(WhichPlayer.Player1); - this["H1"] = new Knight(WhichPlayer.Player1); - this["I1"] = new Lance(WhichPlayer.Player1); + private void InitializeBoardState() + { + this["A1"] = new Lance(WhichPlayer.Player1); + this["B1"] = new Knight(WhichPlayer.Player1); + this["C1"] = new SilverGeneral(WhichPlayer.Player1); + this["D1"] = new GoldGeneral(WhichPlayer.Player1); + this["E1"] = new King(WhichPlayer.Player1); + this["F1"] = new GoldGeneral(WhichPlayer.Player1); + this["G1"] = new SilverGeneral(WhichPlayer.Player1); + this["H1"] = new Knight(WhichPlayer.Player1); + this["I1"] = new Lance(WhichPlayer.Player1); - this["A2"] = null; - this["B2"] = new Bishop(WhichPlayer.Player1); - this["C2"] = null; - this["D2"] = null; - this["E2"] = null; - this["F2"] = null; - this["G2"] = null; - this["H2"] = new Rook(WhichPlayer.Player1); - this["I2"] = null; + this["A2"] = null; + this["B2"] = new Bishop(WhichPlayer.Player1); + this["C2"] = null; + this["D2"] = null; + this["E2"] = null; + this["F2"] = null; + this["G2"] = null; + this["H2"] = new Rook(WhichPlayer.Player1); + this["I2"] = null; - this["A3"] = new Pawn(WhichPlayer.Player1); - this["B3"] = new Pawn(WhichPlayer.Player1); - this["C3"] = new Pawn(WhichPlayer.Player1); - this["D3"] = new Pawn(WhichPlayer.Player1); - this["E3"] = new Pawn(WhichPlayer.Player1); - this["F3"] = new Pawn(WhichPlayer.Player1); - this["G3"] = new Pawn(WhichPlayer.Player1); - this["H3"] = new Pawn(WhichPlayer.Player1); - this["I3"] = new Pawn(WhichPlayer.Player1); + this["A3"] = new Pawn(WhichPlayer.Player1); + this["B3"] = new Pawn(WhichPlayer.Player1); + this["C3"] = new Pawn(WhichPlayer.Player1); + this["D3"] = new Pawn(WhichPlayer.Player1); + this["E3"] = new Pawn(WhichPlayer.Player1); + this["F3"] = new Pawn(WhichPlayer.Player1); + this["G3"] = new Pawn(WhichPlayer.Player1); + this["H3"] = new Pawn(WhichPlayer.Player1); + this["I3"] = new Pawn(WhichPlayer.Player1); - this["A4"] = null; - this["B4"] = null; - this["C4"] = null; - this["D4"] = null; - this["E4"] = null; - this["F4"] = null; - this["G4"] = null; - this["H4"] = null; - this["I4"] = null; + this["A4"] = null; + this["B4"] = null; + this["C4"] = null; + this["D4"] = null; + this["E4"] = null; + this["F4"] = null; + this["G4"] = null; + this["H4"] = null; + this["I4"] = null; - this["A5"] = null; - this["B5"] = null; - this["C5"] = null; - this["D5"] = null; - this["E5"] = null; - this["F5"] = null; - this["G5"] = null; - this["H5"] = null; - this["I5"] = null; + this["A5"] = null; + this["B5"] = null; + this["C5"] = null; + this["D5"] = null; + this["E5"] = null; + this["F5"] = null; + this["G5"] = null; + this["H5"] = null; + this["I5"] = null; - this["A6"] = null; - this["B6"] = null; - this["C6"] = null; - this["D6"] = null; - this["E6"] = null; - this["F6"] = null; - this["G6"] = null; - this["H6"] = null; - this["I6"] = null; + this["A6"] = null; + this["B6"] = null; + this["C6"] = null; + this["D6"] = null; + this["E6"] = null; + this["F6"] = null; + this["G6"] = null; + this["H6"] = null; + this["I6"] = null; - this["A7"] = new Pawn(WhichPlayer.Player2); - this["B7"] = new Pawn(WhichPlayer.Player2); - this["C7"] = new Pawn(WhichPlayer.Player2); - this["D7"] = new Pawn(WhichPlayer.Player2); - this["E7"] = new Pawn(WhichPlayer.Player2); - this["F7"] = new Pawn(WhichPlayer.Player2); - this["G7"] = new Pawn(WhichPlayer.Player2); - this["H7"] = new Pawn(WhichPlayer.Player2); - this["I7"] = new Pawn(WhichPlayer.Player2); + this["A7"] = new Pawn(WhichPlayer.Player2); + this["B7"] = new Pawn(WhichPlayer.Player2); + this["C7"] = new Pawn(WhichPlayer.Player2); + this["D7"] = new Pawn(WhichPlayer.Player2); + this["E7"] = new Pawn(WhichPlayer.Player2); + this["F7"] = new Pawn(WhichPlayer.Player2); + this["G7"] = new Pawn(WhichPlayer.Player2); + this["H7"] = new Pawn(WhichPlayer.Player2); + this["I7"] = new Pawn(WhichPlayer.Player2); - this["A8"] = null; - this["B8"] = new Rook(WhichPlayer.Player2); - this["C8"] = null; - this["D8"] = null; - this["E8"] = null; - this["F8"] = null; - this["G8"] = null; - this["H8"] = new Bishop(WhichPlayer.Player2); - this["I8"] = null; + this["A8"] = null; + this["B8"] = new Rook(WhichPlayer.Player2); + this["C8"] = null; + this["D8"] = null; + this["E8"] = null; + this["F8"] = null; + this["G8"] = null; + this["H8"] = new Bishop(WhichPlayer.Player2); + this["I8"] = null; - this["A9"] = new Lance(WhichPlayer.Player2); - this["B9"] = new Knight(WhichPlayer.Player2); - this["C9"] = new SilverGeneral(WhichPlayer.Player2); - this["D9"] = new GoldGeneral(WhichPlayer.Player2); - this["E9"] = new King(WhichPlayer.Player2); - this["F9"] = new GoldGeneral(WhichPlayer.Player2); - this["G9"] = new SilverGeneral(WhichPlayer.Player2); - this["H9"] = new Knight(WhichPlayer.Player2); - this["I9"] = new Lance(WhichPlayer.Player2); - } - } + this["A9"] = new Lance(WhichPlayer.Player2); + this["B9"] = new Knight(WhichPlayer.Player2); + this["C9"] = new SilverGeneral(WhichPlayer.Player2); + this["D9"] = new GoldGeneral(WhichPlayer.Player2); + this["E9"] = new King(WhichPlayer.Player2); + this["F9"] = new GoldGeneral(WhichPlayer.Player2); + this["G9"] = new SilverGeneral(WhichPlayer.Player2); + this["H9"] = new Knight(WhichPlayer.Player2); + this["I9"] = new Lance(WhichPlayer.Player2); + } + } } diff --git a/Shogi.Domain/DomainExtensions.cs b/Shogi.Domain/DomainExtensions.cs index c530271..f8f9599 100644 --- a/Shogi.Domain/DomainExtensions.cs +++ b/Shogi.Domain/DomainExtensions.cs @@ -1,8 +1,8 @@ -using Shogi.Domain.Pieces; +using Shogi.Domain.ValueObjects; namespace Shogi.Domain { - internal static class DomainExtensions + internal static class DomainExtensions { public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King; diff --git a/Shogi.Domain/Pathing/Path.cs b/Shogi.Domain/Pathing/Path.cs index 9678721..5959c8f 100644 --- a/Shogi.Domain/Pathing/Path.cs +++ b/Shogi.Domain/Pathing/Path.cs @@ -1,9 +1,4 @@ -using Shogi.Domain.Pieces; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Numerics; +using System.Diagnostics; namespace Shogi.Domain.Pathing { diff --git a/Shogi.Domain/Pieces/Bishop.cs b/Shogi.Domain/Pieces/Bishop.cs deleted file mode 100644 index 64768ae..0000000 --- a/Shogi.Domain/Pieces/Bishop.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.Pieces -{ - internal class Bishop : Piece - { - private static readonly ReadOnlyCollection BishopPaths = new(new List(4) - { - new Path(Direction.UpLeft, Distance.MultiStep), - new Path(Direction.UpRight, Distance.MultiStep), - new Path(Direction.DownLeft, Distance.MultiStep), - new Path(Direction.DownRight, Distance.MultiStep) - }); - - public static readonly ReadOnlyCollection PromotedBishopPaths = new(new List(8) - { - new Path(Direction.Up), - new Path(Direction.Left), - new Path(Direction.Right), - new Path(Direction.Down), - new Path(Direction.UpLeft, Distance.MultiStep), - new Path(Direction.UpRight, Distance.MultiStep), - new Path(Direction.DownLeft, Distance.MultiStep), - new Path(Direction.DownRight, Distance.MultiStep) - }); - - public static readonly ReadOnlyCollection Player2Paths = - BishopPaths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public static readonly ReadOnlyCollection Player2PromotedPaths = - PromotedBishopPaths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public Bishop(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Bishop, owner, isPromoted) - { - } - - public override IEnumerable MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths; - } -} diff --git a/Shogi.Domain/Pieces/GoldGeneral.cs b/Shogi.Domain/Pieces/GoldGeneral.cs deleted file mode 100644 index 1ec4308..0000000 --- a/Shogi.Domain/Pieces/GoldGeneral.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.Pieces -{ - internal class GoldGeneral : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(6) - { - new Path(Direction.Up), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.Left), - new Path(Direction.Right), - new Path(Direction.Down) - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public GoldGeneral(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.GoldGeneral, owner, isPromoted) - { - } - - public override IEnumerable MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths; - } -} diff --git a/Shogi.Domain/Pieces/King.cs b/Shogi.Domain/Pieces/King.cs deleted file mode 100644 index 7093b73..0000000 --- a/Shogi.Domain/Pieces/King.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.Pieces -{ - internal class King : Piece - { - internal static readonly ReadOnlyCollection KingPaths = new(new List(8) - { - new Path(Direction.Up), - new Path(Direction.Left), - new Path(Direction.Right), - new Path(Direction.Down), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) - }); - - public King(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.King, owner, isPromoted) - { - } - - public override IEnumerable MoveSet => KingPaths; - } -} diff --git a/Shogi.Domain/Pieces/Knight.cs b/Shogi.Domain/Pieces/Knight.cs deleted file mode 100644 index d5fdb71..0000000 --- a/Shogi.Domain/Pieces/Knight.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.Pieces -{ - internal class Knight : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(2) - { - new Path(Direction.KnightLeft), - new Path(Direction.KnightRight) - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public Knight(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Knight, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/Pieces/Lance.cs b/Shogi.Domain/Pieces/Lance.cs deleted file mode 100644 index 7dcb61b..0000000 --- a/Shogi.Domain/Pieces/Lance.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.Pieces -{ - internal class Lance : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(1) - { - new Path(Direction.Up, Distance.MultiStep), - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public Lance(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Lance, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/Pieces/Pawn.cs b/Shogi.Domain/Pieces/Pawn.cs deleted file mode 100644 index e357691..0000000 --- a/Shogi.Domain/Pieces/Pawn.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.Pieces -{ - internal class Pawn : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(1) - { - new Path(Direction.Up) - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public Pawn(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Pawn, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/Pieces/Piece.cs b/Shogi.Domain/Pieces/Piece.cs deleted file mode 100644 index eb57909..0000000 --- a/Shogi.Domain/Pieces/Piece.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Diagnostics; - -namespace Shogi.Domain.Pieces -{ - [DebuggerDisplay("{WhichPiece} {Owner}")] - public abstract class Piece - { - /// - /// Creates a clone of an existing piece. - /// - public static Piece CreateCopy(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); - public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) - { - return piece switch - { - WhichPiece.King => new King(owner, isPromoted), - WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted), - WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted), - WhichPiece.Bishop => new Bishop(owner, isPromoted), - WhichPiece.Rook => new Rook(owner, isPromoted), - WhichPiece.Knight => new Knight(owner, isPromoted), - WhichPiece.Lance => new Lance(owner, isPromoted), - WhichPiece.Pawn => new Pawn(owner, isPromoted), - _ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.") - }; - } - public abstract IEnumerable MoveSet { get; } - public WhichPiece WhichPiece { get; } - public WhichPlayer Owner { get; private set; } - public bool IsPromoted { get; private set; } - public bool IsUpsideDown => Owner == WhichPlayer.Player2; - - protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) - { - WhichPiece = piece; - Owner = owner; - IsPromoted = isPromoted; - } - - public bool CanPromote => !IsPromoted - && WhichPiece != WhichPiece.King - && WhichPiece != WhichPiece.GoldGeneral; - - public void Promote() => IsPromoted = CanPromote; - - /// - /// Prep the piece for capture by changing ownership and demoting. - /// - public void Capture(WhichPlayer newOwner) - { - Owner = newOwner; - IsPromoted = false; - } - - /// - /// Respecting the move-set of the Piece, collect all positions from start to end. - /// Useful if you need to iterate a move-set. - /// - /// - /// - /// An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions. - public IEnumerable GetPathFromStartToEnd(Vector2 start, Vector2 end) - { - var steps = new List(10); - - var path = this.MoveSet.GetNearestPath(start, end); - var position = start; - while (Vector2.Distance(start, position) < Vector2.Distance(start, end)) - { - position += path.Direction; - steps.Add(position); - - if (path.Distance == Distance.OneStep) break; - } - - if (position == end) - { - return steps; - } - - return Array.Empty(); - } - - /// - /// Get all positions this piece could move to from the currentPosition, respecting the move-set of this piece. - /// - /// - /// A list of positions the piece could move to. - public IEnumerable GetPossiblePositions(Vector2 currentPosition) - { - throw new NotImplementedException(); - } - } -} diff --git a/Shogi.Domain/Pieces/Rook.cs b/Shogi.Domain/Pieces/Rook.cs deleted file mode 100644 index c91f0f2..0000000 --- a/Shogi.Domain/Pieces/Rook.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.Pieces -{ - public sealed class Rook : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(4) - { - new Path(Direction.Up, Distance.MultiStep), - new Path(Direction.Left, Distance.MultiStep), - new Path(Direction.Right, Distance.MultiStep), - new Path(Direction.Down, Distance.MultiStep) - }); - - private static readonly ReadOnlyCollection PromotedPlayer1Paths = new(new List(8) - { - new Path(Direction.Up, Distance.MultiStep), - new Path(Direction.Left, Distance.MultiStep), - new Path(Direction.Right, Distance.MultiStep), - new Path(Direction.Down, Distance.MultiStep), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(m => m.Invert()) - .ToList() - .AsReadOnly(); - - public static readonly ReadOnlyCollection Player2PromotedPaths = - PromotedPlayer1Paths - .Select(m => m.Invert()) - .ToList() - .AsReadOnly(); - - public Rook(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.Rook, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? PromotedPlayer1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? Player2PromotedPaths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/Pieces/SilverGeneral.cs b/Shogi.Domain/Pieces/SilverGeneral.cs deleted file mode 100644 index f2c8623..0000000 --- a/Shogi.Domain/Pieces/SilverGeneral.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Shogi.Domain.Pathing; -using System.Collections.ObjectModel; - -namespace Shogi.Domain.Pieces -{ - internal class SilverGeneral : Piece - { - public static readonly ReadOnlyCollection Player1Paths = new(new List(4) - { - new Path(Direction.Up), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) - }); - - public static readonly ReadOnlyCollection Player2Paths = - Player1Paths - .Select(p => p.Invert()) - .ToList() - .AsReadOnly(); - - public SilverGeneral(WhichPlayer owner, bool isPromoted = false) - : base(WhichPiece.SilverGeneral, owner, isPromoted) - { - } - - public override ReadOnlyCollection MoveSet => Owner switch - { - WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, - WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, - _ => throw new NotImplementedException(), - }; - } -} diff --git a/Shogi.Domain/Session.cs b/Shogi.Domain/Session.cs deleted file mode 100644 index 0584722..0000000 --- a/Shogi.Domain/Session.cs +++ /dev/null @@ -1,257 +0,0 @@ -using Shogi.Domain.Pieces; -using System.Text; - -namespace Shogi.Domain -{ - /// - /// Facilitates Shogi board state transitions, cognisant of Shogi rules. - /// The board is always from Player1's perspective. - /// [0,0] is the lower-left position, [8,8] is the higher-right position - /// - public sealed class Session - { - private readonly Tuple Players; - private readonly BoardState boardState; - private readonly StandardRules rules; - private readonly SessionMetadata metadata; - - public Session() : this(new BoardState()) - { - } - - public Session(BoardState state) : this(state, new SessionMetadata(string.Empty, false, string.Empty, string.Empty)) - { - } - - public Session(BoardState state, SessionMetadata metadata) - { - rules = new StandardRules(state); - boardState = state; - this.metadata = metadata; - Players = new(string.Empty, string.Empty); - } - - public string Name => metadata.Name; - public string Player1Name => metadata.Player1; - public string? Player2Name => metadata.Player2; - public IDictionary BoardState => boardState.State; - public IList Player1Hand => boardState.Player1Hand; - public IList Player2Hand => boardState.Player2Hand; - public WhichPlayer? InCheck => boardState.InCheck; - public bool IsCheckMate => boardState.IsCheckmate; - - /// - /// Move a piece from a board position to another board position, potentially capturing an opponents piece. Respects all rules of the game. - /// - /// - /// The strategy involves simulating a move on a throw-away board state that can be used to - /// validate legal vs illegal moves without having to worry about reverting board state. - /// - /// - public void Move(string from, string to, bool isPromotion) - { - var simulationState = new BoardState(boardState); - var simulation = new StandardRules(simulationState); - var moveResult = simulation.Move(from, to, isPromotion); - if (!moveResult.Success) - { - throw new InvalidOperationException(moveResult.Reason); - } - - // If already in check, assert the move that resulted in check no longer results in check. - if (boardState.InCheck == boardState.WhoseTurn - && simulation.IsOpposingKingThreatenedByPosition(boardState.PreviousMoveTo)) - { - throw new InvalidOperationException("Unable to move because you are still in check."); - } - - var otherPlayer = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - if (simulation.IsPlayerInCheckAfterMove()) - { - throw new InvalidOperationException("Illegal move. This move places you in check."); - } - - _ = rules.Move(from, to, isPromotion); - if (rules.IsOpponentInCheckAfterMove()) - { - boardState.InCheck = otherPlayer; - if (rules.IsOpponentInCheckMate()) - { - boardState.IsCheckmate = true; - } - } - else - { - boardState.InCheck = null; - } - boardState.WhoseTurn = otherPlayer; - } - - public void Move(WhichPiece pieceInHand, string to) - { - var index = boardState.ActivePlayerHand.FindIndex(p => p.WhichPiece == pieceInHand); - if (index == -1) - { - throw new InvalidOperationException($"{pieceInHand} does not exist in the hand."); - } - - if (boardState[to] != null) - { - throw new InvalidOperationException("Illegal placement of piece from the hand. Destination is not empty."); - } - - var toVector = Notation.FromBoardNotation(to); - switch (pieceInHand) - { - case WhichPiece.Knight: - { - // Knight cannot be placed onto the farthest two ranks from the hand. - if ((boardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y > 6) - || (boardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y < 2)) - { - throw new InvalidOperationException("Illegal move. Knight has no valid moves after placement."); - } - break; - } - case WhichPiece.Lance: - case WhichPiece.Pawn: - { - // Lance and Pawn cannot be placed onto the farthest rank from the hand. - if ((boardState.WhoseTurn == WhichPlayer.Player1 && toVector.Y == 8) - || (boardState.WhoseTurn == WhichPlayer.Player2 && toVector.Y == 0)) - { - throw new InvalidOperationException($"Illegal move. {pieceInHand} has no valid moves after placement."); - } - break; - } - } - - var tempBoard = new BoardState(boardState); - var simulation = new StandardRules(tempBoard); - var moveResult = simulation.Move(pieceInHand, to); - if (!moveResult.Success) - { - throw new InvalidOperationException(moveResult.Reason); - } - - var otherPlayer = tempBoard.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1; - if (boardState.InCheck == boardState.WhoseTurn) - { - //if (simulation.IsPlayerInCheckAfterMove(boardState.PreviousMoveTo, toVector, boardState.WhoseTurn)) - //{ - // throw new InvalidOperationException("Illegal move. You're still in check!"); - //} - } - - var kingPosition = otherPlayer == WhichPlayer.Player1 ? tempBoard.Player1KingPosition : tempBoard.Player2KingPosition; - //if (simulation.IsPlayerInCheckAfterMove(toVector, kingPosition, otherPlayer)) - //{ - - //} - - //rules.Move(from, to, isPromotion); - //if (rules.IsPlayerInCheckAfterMove(fromVector, toVector, otherPlayer)) - //{ - // board.InCheck = otherPlayer; - // board.IsCheckmate = rules.EvaluateCheckmate(); - //} - //else - //{ - // board.InCheck = null; - //} - boardState.WhoseTurn = otherPlayer; - } - - /// - /// Prints a ASCII representation of the board for debugging board state. - /// - /// - public string ToStringStateAsAscii() - { - var builder = new StringBuilder(); - builder.Append(" "); - builder.Append("Player 2(.)"); - builder.AppendLine(); - for (var rank = 8; rank >= 0; rank--) - { - // Horizontal line - builder.Append(" - "); - for (var file = 0; file < 8; file++) builder.Append("- - "); - builder.Append("- -"); - - // Print Rank ruler. - builder.AppendLine(); - builder.Append($"{rank + 1} "); - - // Print pieces. - builder.Append(" |"); - for (var x = 0; x < 9; x++) - { - var piece = boardState[x, rank]; - if (piece == null) - { - builder.Append(" "); - } - else - { - builder.AppendFormat("{0}", ToAscii(piece)); - } - builder.Append('|'); - } - builder.AppendLine(); - } - - // Horizontal line - builder.Append(" - "); - for (var x = 0; x < 8; x++) builder.Append("- - "); - builder.Append("- -"); - builder.AppendLine(); - builder.Append(" "); - builder.Append("Player 1"); - - builder.AppendLine(); - builder.AppendLine(); - // Print File ruler. - builder.Append(" "); - builder.Append(" A B C D E F G H I "); - - return builder.ToString(); - } - - /// - /// - /// - /// - /// - /// A string with three characters. - /// The first character indicates promotion status. - /// The second character indicates piece. - /// The third character indicates ownership. - /// - public static string ToAscii(Piece piece) - { - var builder = new StringBuilder(); - if (piece.IsPromoted) builder.Append('^'); - else builder.Append(' '); - - var name = piece.WhichPiece switch - { - WhichPiece.King => "K", - WhichPiece.GoldGeneral => "G", - WhichPiece.SilverGeneral => "S", - WhichPiece.Bishop => "B", - WhichPiece.Rook => "R", - WhichPiece.Knight => "k", - WhichPiece.Lance => "L", - WhichPiece.Pawn => "P", - _ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}."), - }; - builder.Append(name); - - if (piece.Owner == WhichPlayer.Player2) builder.Append('.'); - else builder.Append(' '); - - return builder.ToString(); - } - } -} diff --git a/Shogi.Domain/SessionMetadata.cs b/Shogi.Domain/SessionMetadata.cs deleted file mode 100644 index bcace67..0000000 --- a/Shogi.Domain/SessionMetadata.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Shogi.Domain -{ - /// - /// A representation of a Session without the board and game-rules. - /// - public class SessionMetadata - { - public string Name { get; } - public string Player1 { get; } - public string? Player2 { get; private set; } - public bool IsPrivate { get; } - - public SessionMetadata(string name, bool isPrivate, string player1, string? player2 = null) - { - Name = name; - IsPrivate = isPrivate; - Player1 = player1; - Player2 = player2; - } - - public void SetPlayer2(string user) - { - Player2 = user; - } - - public bool IsSeated(string playerName) => playerName == Player1 || playerName == Player2; - } -} diff --git a/Shogi.Domain/Shogi.Domain.csproj b/Shogi.Domain/Shogi.Domain.csproj index 5ca4174..0c0a0ef 100644 --- a/Shogi.Domain/Shogi.Domain.csproj +++ b/Shogi.Domain/Shogi.Domain.csproj @@ -7,10 +7,14 @@ - - - - + + + + + + + + diff --git a/Shogi.Domain/StandardRules.cs b/Shogi.Domain/StandardRules.cs index 9d9b588..2149dd0 100644 --- a/Shogi.Domain/StandardRules.cs +++ b/Shogi.Domain/StandardRules.cs @@ -1,10 +1,10 @@ using Shogi.Domain.Pathing; -using Shogi.Domain.Pieces; -using BoardTile = System.Collections.Generic.KeyValuePair; +using Shogi.Domain.ValueObjects; +using BoardTile = System.Collections.Generic.KeyValuePair; namespace Shogi.Domain { - internal class StandardRules + internal class StandardRules { private readonly BoardState boardState; diff --git a/Shogi.Domain/ValueObjects/Bishop.cs b/Shogi.Domain/ValueObjects/Bishop.cs new file mode 100644 index 0000000..076ceeb --- /dev/null +++ b/Shogi.Domain/ValueObjects/Bishop.cs @@ -0,0 +1,47 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.ValueObjects +{ + internal record class Bishop : Piece + { + private static readonly ReadOnlyCollection BishopPaths = new(new List(4) + { + new Path(Direction.UpLeft, Distance.MultiStep), + new Path(Direction.UpRight, Distance.MultiStep), + new Path(Direction.DownLeft, Distance.MultiStep), + new Path(Direction.DownRight, Distance.MultiStep) + }); + + public static readonly ReadOnlyCollection PromotedBishopPaths = new(new List(8) + { + new Path(Direction.Up), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down), + new Path(Direction.UpLeft, Distance.MultiStep), + new Path(Direction.UpRight, Distance.MultiStep), + new Path(Direction.DownLeft, Distance.MultiStep), + new Path(Direction.DownRight, Distance.MultiStep) + }); + + public static readonly ReadOnlyCollection Player2Paths = + BishopPaths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public static readonly ReadOnlyCollection Player2PromotedPaths = + PromotedBishopPaths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Bishop(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Bishop, owner, isPromoted) + { + } + + public override IEnumerable MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths; + } +} diff --git a/Shogi.Domain/ValueObjects/GoldGeneral.cs b/Shogi.Domain/ValueObjects/GoldGeneral.cs new file mode 100644 index 0000000..52c19be --- /dev/null +++ b/Shogi.Domain/ValueObjects/GoldGeneral.cs @@ -0,0 +1,31 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.ValueObjects +{ + internal record class GoldGeneral : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(6) + { + new Path(Direction.Up), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public GoldGeneral(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.GoldGeneral, owner, isPromoted) + { + } + + public override IEnumerable MoveSet => Owner == WhichPlayer.Player1 ? Player1Paths : Player2Paths; + } +} diff --git a/Shogi.Domain/ValueObjects/King.cs b/Shogi.Domain/ValueObjects/King.cs new file mode 100644 index 0000000..af3c91b --- /dev/null +++ b/Shogi.Domain/ValueObjects/King.cs @@ -0,0 +1,27 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.ValueObjects +{ + internal record class King : Piece + { + internal static readonly ReadOnlyCollection KingPaths = new(new List(8) + { + new Path(Direction.Up), + new Path(Direction.Left), + new Path(Direction.Right), + new Path(Direction.Down), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }); + + public King(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.King, owner, isPromoted) + { + } + + public override IEnumerable MoveSet => KingPaths; + } +} diff --git a/Shogi.Domain/ValueObjects/Knight.cs b/Shogi.Domain/ValueObjects/Knight.cs new file mode 100644 index 0000000..dbdcf74 --- /dev/null +++ b/Shogi.Domain/ValueObjects/Knight.cs @@ -0,0 +1,32 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.ValueObjects +{ + internal record class Knight : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(2) + { + new Path(Direction.KnightLeft), + new Path(Direction.KnightRight) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Knight(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Knight, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Domain/ValueObjects/Lance.cs b/Shogi.Domain/ValueObjects/Lance.cs new file mode 100644 index 0000000..52cc1f0 --- /dev/null +++ b/Shogi.Domain/ValueObjects/Lance.cs @@ -0,0 +1,31 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.ValueObjects +{ + internal record class Lance : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(1) + { + new Path(Direction.Up, Distance.MultiStep), + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Lance(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Lance, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Domain/ValueObjects/Pawn.cs b/Shogi.Domain/ValueObjects/Pawn.cs new file mode 100644 index 0000000..566792b --- /dev/null +++ b/Shogi.Domain/ValueObjects/Pawn.cs @@ -0,0 +1,31 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.ValueObjects +{ + internal record class Pawn : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(1) + { + new Path(Direction.Up) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public Pawn(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Pawn, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Shogi.Domain/ValueObjects/Piece.cs b/Shogi.Domain/ValueObjects/Piece.cs new file mode 100644 index 0000000..0ba7769 --- /dev/null +++ b/Shogi.Domain/ValueObjects/Piece.cs @@ -0,0 +1,95 @@ +using Shogi.Domain.Pathing; +using System.Diagnostics; + +namespace Shogi.Domain.ValueObjects +{ + [DebuggerDisplay("{WhichPiece} {Owner}")] + public abstract record class Piece + { + /// + /// Creates a clone of an existing piece. + /// + public static Piece CreateCopy(Piece piece) => Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); + public static Piece Create(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) + { + return piece switch + { + WhichPiece.King => new King(owner, isPromoted), + WhichPiece.GoldGeneral => new GoldGeneral(owner, isPromoted), + WhichPiece.SilverGeneral => new SilverGeneral(owner, isPromoted), + WhichPiece.Bishop => new Bishop(owner, isPromoted), + WhichPiece.Rook => new Rook(owner, isPromoted), + WhichPiece.Knight => new Knight(owner, isPromoted), + WhichPiece.Lance => new Lance(owner, isPromoted), + WhichPiece.Pawn => new Pawn(owner, isPromoted), + _ => throw new ArgumentException($"Unknown {nameof(WhichPiece)} when cloning a {nameof(Piece)}.") + }; + } + public abstract IEnumerable MoveSet { get; } + public WhichPiece WhichPiece { get; } + public WhichPlayer Owner { get; private set; } + public bool IsPromoted { get; private set; } + public bool IsUpsideDown => Owner == WhichPlayer.Player2; + + protected Piece(WhichPiece piece, WhichPlayer owner, bool isPromoted = false) + { + WhichPiece = piece; + Owner = owner; + IsPromoted = isPromoted; + } + + public bool CanPromote => !IsPromoted + && WhichPiece != WhichPiece.King + && WhichPiece != WhichPiece.GoldGeneral; + + public void Promote() => IsPromoted = CanPromote; + + /// + /// Prep the piece for capture by changing ownership and demoting. + /// + public void Capture(WhichPlayer newOwner) + { + Owner = newOwner; + IsPromoted = false; + } + + /// + /// Respecting the move-set of the Piece, collect all positions from start to end. + /// Useful if you need to iterate a move-set. + /// + /// + /// + /// An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions. + public IEnumerable GetPathFromStartToEnd(Vector2 start, Vector2 end) + { + var steps = new List(10); + + var path = MoveSet.GetNearestPath(start, end); + var position = start; + while (Vector2.Distance(start, position) < Vector2.Distance(start, end)) + { + position += path.Direction; + steps.Add(position); + + if (path.Distance == Distance.OneStep) break; + } + + if (position == end) + { + return steps; + } + + return Array.Empty(); + } + + /// + /// Get all positions this piece could move to from the currentPosition, respecting the move-set of this piece. + /// + /// + /// A list of positions the piece could move to. + public IEnumerable GetPossiblePositions(Vector2 currentPosition) + { + throw new NotImplementedException(); + } + } +} diff --git a/Shogi.Domain/ValueObjects/Rook.cs b/Shogi.Domain/ValueObjects/Rook.cs new file mode 100644 index 0000000..505d944 --- /dev/null +++ b/Shogi.Domain/ValueObjects/Rook.cs @@ -0,0 +1,51 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.ValueObjects; + +public record class Rook : Piece +{ + public static readonly ReadOnlyCollection Player1Paths = new(new List(4) + { + new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Left, Distance.MultiStep), + new Path(Direction.Right, Distance.MultiStep), + new Path(Direction.Down, Distance.MultiStep) + }); + + private static readonly ReadOnlyCollection PromotedPlayer1Paths = new(new List(8) + { + new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Left, Distance.MultiStep), + new Path(Direction.Right, Distance.MultiStep), + new Path(Direction.Down, Distance.MultiStep), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(m => m.Invert()) + .ToList() + .AsReadOnly(); + + public static readonly ReadOnlyCollection Player2PromotedPaths = + PromotedPlayer1Paths + .Select(m => m.Invert()) + .ToList() + .AsReadOnly(); + + public Rook(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.Rook, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? PromotedPlayer1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? Player2PromotedPaths : Player2Paths, + _ => throw new NotImplementedException(), + }; +} diff --git a/Shogi.Domain/ValueObjects/SilverGeneral.cs b/Shogi.Domain/ValueObjects/SilverGeneral.cs new file mode 100644 index 0000000..71b55df --- /dev/null +++ b/Shogi.Domain/ValueObjects/SilverGeneral.cs @@ -0,0 +1,35 @@ +using Shogi.Domain.Pathing; +using System.Collections.ObjectModel; + +namespace Shogi.Domain.ValueObjects +{ + internal record class SilverGeneral : Piece + { + public static readonly ReadOnlyCollection Player1Paths = new(new List(4) + { + new Path(Direction.Up), + new Path(Direction.UpLeft), + new Path(Direction.UpRight), + new Path(Direction.DownLeft), + new Path(Direction.DownRight) + }); + + public static readonly ReadOnlyCollection Player2Paths = + Player1Paths + .Select(p => p.Invert()) + .ToList() + .AsReadOnly(); + + public SilverGeneral(WhichPlayer owner, bool isPromoted = false) + : base(WhichPiece.SilverGeneral, owner, isPromoted) + { + } + + public override ReadOnlyCollection MoveSet => Owner switch + { + WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths, + WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/Gameboard.ShogiUI.Sockets/.config/dotnet-tools.json b/Shogi.Sockets/.config/dotnet-tools.json similarity index 100% rename from Gameboard.ShogiUI.Sockets/.config/dotnet-tools.json rename to Shogi.Sockets/.config/dotnet-tools.json diff --git a/Shogi.Sockets/Controllers/SessionController.cs b/Shogi.Sockets/Controllers/SessionController.cs new file mode 100644 index 0000000..f66c91b --- /dev/null +++ b/Shogi.Sockets/Controllers/SessionController.cs @@ -0,0 +1,217 @@ +using Shogi.Api.Managers; +using Shogi.Api.Repositories; +using Shogi.Contracts.Api; +using Shogi.Contracts.Socket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Data.SqlClient; +using Shogi.Contracts.Types; +using Shogi.Api.Extensions; + +namespace Shogi.Api.Controllers; + +[ApiController] +[Route("[controller]")] +[Authorize] +public class SessionController : ControllerBase +{ + private readonly ISocketConnectionManager communicationManager; + private readonly IModelMapper mapper; + private readonly ISessionRepository sessionRepository; + private readonly IQueryRespository queryRespository; + + public SessionController( + ISocketConnectionManager communicationManager, + IModelMapper mapper, + ISessionRepository sessionRepository, + IQueryRespository queryRespository) + { + this.communicationManager = communicationManager; + this.mapper = mapper; + this.sessionRepository = sessionRepository; + this.queryRespository = queryRespository; + } + + [HttpPost] + public async Task CreateSession([FromBody] CreateSessionCommand request) + { + var userId = User.GetShogiUserId(); + if (string.IsNullOrWhiteSpace(userId)) return this.Unauthorized(); + var session = new Domain.Session(request.Name, Domain.BoardState.StandardStarting, userId); + try + { + await sessionRepository.CreateSession(session); + } + catch (SqlException) + { + return this.Conflict(); + } + + await communicationManager.BroadcastToAll(new SessionCreatedSocketMessage()); + return CreatedAtAction(nameof(CreateSession), new { sessionName = request.Name }, null); + } + + //[HttpPost("{sessionName}/Move")] + //public async Task MovePiece([FromRoute] string sessionName, [FromBody] MovePieceCommand request) + //{ + + // var user = await gameboardManager.ReadUser(User); + // var session = await gameboardRepository.ReadSession(sessionName); + // if (session == null) + // { + // return NotFound(); + // } + // if (user == null || (session.Player1 != user.Id && session.Player2 != user.Id)) + // { + // return Forbid("User is not seated at this game."); + // } + + // try + // { + // var move = request.Move; + // if (move.PieceFromCaptured.HasValue) + // session.Move(mapper.Map(move.PieceFromCaptured.Value), move.To); + // else if (!string.IsNullOrWhiteSpace(move.From)) + // session.Move(move.From, move.To, move.IsPromotion); + + // await gameboardRepository.CreateBoardState(session); + // await communicationManager.BroadcastToPlayers( + // new MoveResponse + // { + // SessionName = session.Name, + // PlayerName = user.Id + // }, + // session.Player1, + // session.Player2); + + // return Ok(); + // } + // catch (InvalidOperationException ex) + // { + // return Conflict(ex.Message); + // } + //} + + // TODO: Use JWT tokens for guests so they can authenticate and use API routes, too. + //[Route("")] + //public async Task PostSession([FromBody] PostSession request) + //{ + // var model = new Models.Session(request.Name, request.IsPrivate, request.Player1, request.Player2); + // var success = await repository.CreateSession(model); + // if (success) + // { + // var message = new ServiceModels.Socket.Messages.CreateGameResponse(ServiceModels.Types.SocketAction.CreateGame) + // { + // Game = model.ToServiceModel(), + // PlayerName = + // } + // var task = request.IsPrivate + // ? communicationManager.BroadcastToPlayers(response, userName) + // : communicationManager.BroadcastToAll(response); + // return new CreatedResult("", null); + // } + // return new ConflictResult(); + //} + + + + //[HttpGet("{sessionName}")] + //[AllowAnonymous] + //public async Task GetSession([FromRoute] string sessionName) + //{ + // var user = await ReadUserOrThrow(); + // var session = await gameboardRepository.ReadSession(sessionName); + // if (session == null) + // { + // return NotFound(); + // } + + // var playerPerspective = session.Player2 == user.Id + // ? WhichPlayer.Player2 + // : WhichPlayer.Player1; + + // var response = new ReadSessionResponse + // { + // Session = new Session + // { + // BoardState = new BoardState + // { + // Board = mapper.Map(session.BoardState.State), + // Player1Hand = session.BoardState.Player1Hand.Select(mapper.Map).ToList(), + // Player2Hand = session.BoardState.Player2Hand.Select(mapper.Map).ToList(), + // PlayerInCheck = mapper.Map(session.BoardState.InCheck) + // }, + // SessionName = session.Name, + // Player1 = session.Player1, + // Player2 = session.Player2 + // } + // }; + // return Ok(response); + //} + + [HttpGet] + [AllowAnonymous] + public async Task> GetSessions() + { + var sessions = await this.queryRespository.ReadAllSessionsMetadata(); + + return Ok(new ReadAllSessionsResponse + { + PlayerHasJoinedSessions = Array.Empty(), + AllOtherSessions = sessions.ToList() + }); + } + + //[HttpPut("{sessionName}")] + //public async Task PutJoinSession([FromRoute] string sessionName) + //{ + // var user = await ReadUserOrThrow(); + // var session = await gameboardRepository.ReadSessionMetaData(sessionName); + // 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.Id); + // await gameboardRepository.UpdateSession(session); + + // var opponentName = user.Id == session.Player1 + // ? session.Player2! + // : session.Player1; + // await communicationManager.BroadcastToPlayers(new JoinSessionResponse + // { + // SessionName = session.Name, + // PlayerName = user.Id + // }, opponentName); + // return Ok(); + //} + + //[Authorize(Roles = "Admin")] + //[HttpDelete("{sessionName}")] + //public async Task DeleteSession([FromRoute] string sessionName) + //{ + // var user = await ReadUserOrThrow(); + // if (user.IsAdmin) + // { + // return Ok(); + // } + // else + // { + // return Unauthorized(); + // } + //} + + //private async Task ReadUserOrThrow() + //{ + // var user = await gameboardManager.ReadUser(User); + // if (user == null) + // { + // throw new UnauthorizedAccessException("Unknown user claims."); + // } + // return user; + //} +} diff --git a/Shogi.Sockets/Controllers/UserController.cs b/Shogi.Sockets/Controllers/UserController.cs new file mode 100644 index 0000000..4c69499 --- /dev/null +++ b/Shogi.Sockets/Controllers/UserController.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Shogi.Contracts.Api; +using Shogi.Api.Extensions; +using Shogi.Api.Managers; +using Shogi.Api.Models; +using Shogi.Api.Repositories; +using System.Security.Claims; + +namespace Shogi.Api.Controllers; + +[ApiController] +[Route("[controller]")] +[Authorize] +public class UserController : ControllerBase +{ + private readonly ISocketTokenCache tokenCache; + private readonly ISocketConnectionManager connectionManager; + private readonly IUserRepository userRepository; + private readonly IShogiUserClaimsTransformer claimsTransformation; + private readonly AuthenticationProperties authenticationProps; + + public UserController( + ILogger logger, + ISocketTokenCache tokenCache, + ISocketConnectionManager connectionManager, + IUserRepository userRepository, + IShogiUserClaimsTransformer claimsTransformation) + { + this.tokenCache = tokenCache; + this.connectionManager = connectionManager; + this.userRepository = userRepository; + this.claimsTransformation = claimsTransformation; + authenticationProps = new AuthenticationProperties + { + AllowRefresh = true, + IsPersistent = true + }; + } + + [HttpPut("GuestLogout")] + public async Task GuestLogout() + { + var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + var userId = User?.GetGuestUserId(); + if (!string.IsNullOrEmpty(userId)) + { + connectionManager.Unsubscribe(userId); + } + + await signoutTask; + return Ok(); + } + + //[HttpGet("Token")] + //public async Task GetToken() + //{ + // var user = await gameboardManager.ReadUser(User); + // if (user == null) + // { + // await gameboardManager.CreateUser(User); + // user = await gameboardManager.ReadUser(User); + // } + + // if (user == null) + // { + // return Unauthorized(); + // } + + // var token = tokenCache.GenerateToken(user.Id); + // return new JsonResult(new CreateTokenResponse(token)); + //} + + [AllowAnonymous] + [HttpGet("LoginAsGuest")] + public async Task GuestLogin() + { + var principal = await this.claimsTransformation.CreateClaimsFromGuestPrincipal(User); + if (principal != null) + { + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + principal, + authenticationProps + ); + } + return Ok(); + + + } + [HttpGet("GuestToken")] + public IActionResult GetGuestToken() + { + var id = User.GetGuestUserId(); + var displayName = User.DisplayName(); + if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(displayName)) + { + var token = tokenCache.GenerateToken(User.GetGuestUserId()!); + return this.Ok(new CreateGuestTokenResponse(id, displayName, token)); + } + + return this.Unauthorized(); + } +} diff --git a/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs b/Shogi.Sockets/ExampleAnonymousSessionMiddleware.cs similarity index 88% rename from Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs rename to Shogi.Sockets/ExampleAnonymousSessionMiddleware.cs index f39e649..9565c0a 100644 --- a/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs +++ b/Shogi.Sockets/ExampleAnonymousSessionMiddleware.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets +namespace Shogi.Api { namespace anonymous_session.Middlewares { @@ -9,11 +9,11 @@ /// /// TODO: Use this example in the guest session logic instead of custom claims. /// - public class AnonymousSessionMiddleware + public class ExampleAnonymousSessionMiddleware { private readonly RequestDelegate _next; - public AnonymousSessionMiddleware(RequestDelegate next) + public ExampleAnonymousSessionMiddleware(RequestDelegate next) { _next = next; } diff --git a/Shogi.Sockets/Extensions/Extensions.cs b/Shogi.Sockets/Extensions/Extensions.cs new file mode 100644 index 0000000..49f2c51 --- /dev/null +++ b/Shogi.Sockets/Extensions/Extensions.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; + +namespace Shogi.Api.Extensions; + +public static class Extensions +{ + private static readonly string MsalUsernameClaim = "preferred_username"; + + public static string? GetGuestUserId(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 IsMicrosoft(this ClaimsPrincipal self) + { + return self.HasClaim(c => c.Type == MsalUsernameClaim); + } + + public static string? GetMicrosoftUserId(this ClaimsPrincipal self) + { + return self.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim)?.Value; + } + + public static string? GetShogiUserId(this ClaimsPrincipal self) => self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId(); +} diff --git a/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs b/Shogi.Sockets/Extensions/LogMiddleware.cs similarity index 95% rename from Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs rename to Shogi.Sockets/Extensions/LogMiddleware.cs index d4860c4..a3111ec 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs +++ b/Shogi.Sockets/Extensions/LogMiddleware.cs @@ -5,7 +5,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; -namespace Gameboard.ShogiUI.Sockets.Extensions +namespace Shogi.Api.Extensions { public class LogMiddleware { diff --git a/Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs b/Shogi.Sockets/Extensions/WebSocketExtensions.cs similarity index 81% rename from Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs rename to Shogi.Sockets/Extensions/WebSocketExtensions.cs index fc53a38..5b22b67 100644 --- a/Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs +++ b/Shogi.Sockets/Extensions/WebSocketExtensions.cs @@ -1,10 +1,7 @@ -using System; -using System.Net.WebSockets; +using System.Net.WebSockets; using System.Text; -using System.Threading; -using System.Threading.Tasks; -namespace Gameboard.ShogiUI.Sockets.Extensions +namespace Shogi.Api.Extensions { public static class WebSocketExtensions { diff --git a/Gameboard.ShogiUI.Sockets/Managers/ModelMapper.cs b/Shogi.Sockets/Managers/ModelMapper.cs similarity index 71% rename from Gameboard.ShogiUI.Sockets/Managers/ModelMapper.cs rename to Shogi.Sockets/Managers/ModelMapper.cs index 8d70143..61ad350 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/ModelMapper.cs +++ b/Shogi.Sockets/Managers/ModelMapper.cs @@ -1,8 +1,9 @@ -using Gameboard.ShogiUI.Sockets.ServiceModels.Types; +using Shogi.Contracts.Types; using DomainWhichPiece = Shogi.Domain.WhichPiece; using DomainWhichPlayer = Shogi.Domain.WhichPlayer; +using Piece = Shogi.Contracts.Types.Piece; -namespace Gameboard.ShogiUI.Sockets.Managers +namespace Shogi.Api.Managers { public class ModelMapper : IModelMapper { @@ -55,28 +56,17 @@ namespace Gameboard.ShogiUI.Sockets.Managers }; } - public SessionMetadata Map(Shogi.Domain.SessionMetadata session) - { - return new SessionMetadata - { - Name = session.Name, - Player1 = session.Player1, - Player2 = session.Player2, - IsPrivate = session.IsPrivate - }; - } - - public Piece Map(Shogi.Domain.Pieces.Piece piece) + public Piece Map(Domain.ValueObjects.Piece piece) { return new Piece { IsPromoted = piece.IsPromoted, Owner = Map(piece.Owner), WhichPiece = Map(piece.WhichPiece) }; } - public Dictionary Map(IDictionary boardState) + public Dictionary Map(IDictionary boardState) { - return boardState.ToDictionary(kvp => kvp.Key, kvp => MapNullable(kvp.Value)); + return boardState.ToDictionary(kvp => kvp.Key.ToUpper(), kvp => MapNullable(kvp.Value)); } - public Piece? MapNullable(Shogi.Domain.Pieces.Piece? piece) + public Piece? MapNullable(Domain.ValueObjects.Piece? piece) { if (piece == null) return null; return Map(piece); @@ -89,9 +79,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers WhichPlayer? Map(DomainWhichPlayer? whichPlayer); WhichPiece Map(DomainWhichPiece whichPiece); DomainWhichPiece Map(WhichPiece value); - SessionMetadata Map(Shogi.Domain.SessionMetadata session); - Piece Map(Shogi.Domain.Pieces.Piece p); - Piece? MapNullable(Shogi.Domain.Pieces.Piece? p); - Dictionary Map(IDictionary boardState); + Piece Map(Domain.ValueObjects.Piece p); + Piece? MapNullable(Domain.ValueObjects.Piece? p); + Dictionary Map(IDictionary boardState); } } diff --git a/Shogi.Sockets/Managers/SocketConnectionManager.cs b/Shogi.Sockets/Managers/SocketConnectionManager.cs new file mode 100644 index 0000000..8b8709a --- /dev/null +++ b/Shogi.Sockets/Managers/SocketConnectionManager.cs @@ -0,0 +1,89 @@ +using Shogi.Contracts.Socket; +using Shogi.Api.Extensions; +using System.Collections.Concurrent; +using System.Net.WebSockets; +using System.Text.Json; + +namespace Shogi.Api.Managers; + +public interface ISocketConnectionManager +{ + Task BroadcastToAll(ISocketResponse response); + void Subscribe(WebSocket socket, string playerName); + void Unsubscribe(string playerName); + Task BroadcastToPlayers(ISocketResponse response, params string?[] playerNames); +} + +/// +/// Retains all active socket connections and provides convenient methods for sending messages to clients. +/// +public class SocketConnectionManager : ISocketConnectionManager +{ + /// Dictionary key is player name. + private readonly ConcurrentDictionary connections; + private readonly JsonSerializerOptions serializeOptions; + + /// Dictionary key is game name. + private readonly ILogger logger; + + public SocketConnectionManager(ILogger logger) + { + this.logger = logger; + this.connections = new ConcurrentDictionary(); + this.serializeOptions = new JsonSerializerOptions(JsonSerializerDefaults.General); + + } + + public void Subscribe(WebSocket socket, string playerName) + { + connections.TryRemove(playerName, out var _); + connections.TryAdd(playerName, socket); + } + + public void Unsubscribe(string playerName) + { + connections.TryRemove(playerName, out _); + } + + public async Task BroadcastToPlayers(ISocketResponse response, params string?[] playerNames) + { + var tasks = new List(playerNames.Length); + foreach (var name in playerNames) + { + if (!string.IsNullOrEmpty(name) && connections.TryGetValue(name, out var socket)) + { + var serialized = Serialize(response); + logger.LogInformation("Response to {0} \n{1}\n", name, serialized); + tasks.Add(socket.SendTextAsync(serialized)); + } + } + await Task.WhenAll(tasks); + } + public Task BroadcastToAll(ISocketResponse response) + { + var message = Serialize(response); + logger.LogInformation("Broadcasting:\n{0}\nDone Broadcasting.", message); + var tasks = new List(connections.Count); + foreach (var kvp in connections) + { + var socket = kvp.Value; + try + { + tasks.Add(socket.SendTextAsync(message)); + } + catch (WebSocketException) + { + logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); + Unsubscribe(kvp.Key); + } + catch + { + logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key); + Unsubscribe(kvp.Key); + } + } + return Task.WhenAll(tasks); + } + + private string Serialize(object o) => JsonSerializer.Serialize(o, this.serializeOptions); +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs b/Shogi.Sockets/Managers/SocketTokenCache.cs similarity index 96% rename from Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs rename to Shogi.Sockets/Managers/SocketTokenCache.cs index 7f6bead..15c9291 100644 --- a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenCache.cs +++ b/Shogi.Sockets/Managers/SocketTokenCache.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -namespace Gameboard.ShogiUI.Sockets.Managers +namespace Shogi.Api.Managers { public interface ISocketTokenCache { diff --git a/Shogi.Sockets/Models/User.cs b/Shogi.Sockets/Models/User.cs new file mode 100644 index 0000000..568074e --- /dev/null +++ b/Shogi.Sockets/Models/User.cs @@ -0,0 +1,42 @@ +using System.Collections.ObjectModel; + +namespace Shogi.Api.Models; + +public class User +{ + public static readonly ReadOnlyCollection Adjectives = new(new[] { + "Fortuitous", "Retractable", "Happy", "Habbitable", "Creative", "Fluffy", "Impervious", "Kingly" + }); + public static readonly ReadOnlyCollection Subjects = new(new[] { + "Hippo", "Basil", "Mouse", "Walnut", "Prince", "Lima Bean", "Coala", "Potato", "Penguin" + }); + public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft); + public static User CreateGuestUser(string id) + { + var random = new Random(); + // Adjective + var index = (int)Math.Floor(random.NextDouble() * Adjectives.Count); + var adj = Adjectives[index]; + // Subject + index = (int)Math.Floor(random.NextDouble() * Subjects.Count); + var subj = Subjects[index]; + + return new User(id, $"{adj} {subj}", WhichLoginPlatform.Guest); + } + + public string Id { get; } + public string DisplayName { get; } + + public WhichLoginPlatform LoginPlatform { get; } + + public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest; + + public bool IsAdmin => LoginPlatform == WhichLoginPlatform.Microsoft && Id == "Hauth@live.com"; + + public User(string id, string displayName, WhichLoginPlatform platform) + { + Id = id; + DisplayName = displayName; + LoginPlatform = platform; + } +} diff --git a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs b/Shogi.Sockets/Models/WhichLoginPlatform.cs similarity index 64% rename from Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs rename to Shogi.Sockets/Models/WhichLoginPlatform.cs index 5d2378e..5972475 100644 --- a/Gameboard.ShogiUI.Sockets/Models/WhichLoginPlatform.cs +++ b/Shogi.Sockets/Models/WhichLoginPlatform.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.Models +namespace Shogi.Api.Models { public enum WhichLoginPlatform { diff --git a/Gameboard.ShogiUI.Sockets/Program.cs b/Shogi.Sockets/Program.cs similarity index 63% rename from Gameboard.ShogiUI.Sockets/Program.cs rename to Shogi.Sockets/Program.cs index 749546a..fc6584f 100644 --- a/Gameboard.ShogiUI.Sockets/Program.cs +++ b/Shogi.Sockets/Program.cs @@ -1,22 +1,19 @@ -using FluentValidation; -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.Http.Extensions; using Microsoft.AspNetCore.HttpLogging; using Microsoft.Identity.Web; using Microsoft.OpenApi.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; +using Shogi.Api.Managers; +using Shogi.Api.Repositories; +using Shogi.Api.Services; using System.Text; -namespace Gameboard.ShogiUI.Sockets +namespace Shogi.Api { public class Program { @@ -24,6 +21,22 @@ namespace Gameboard.ShogiUI.Sockets { var builder = WebApplication.CreateBuilder(args); + var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get(); + Console.WriteLine(string.Join("\n", allowedOrigins)); + builder.Services.AddCors(options => + { + options.AddDefaultPolicy(policy => + { + policy + .WithOrigins(allowedOrigins) + .SetIsOriginAllowedToAllowWildcardSubdomains() + .WithExposedHeaders("Set-Cookie") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + ConfigureAuthentication(builder); ConfigureControllersWithNewtonsoft(builder); ConfigureSwagger(builder); @@ -32,7 +45,10 @@ namespace Gameboard.ShogiUI.Sockets var app = builder.Build(); - app.UseHttpLogging(); + app.UseWhen( + // Log anything that isn't related to swagger. + context => ShouldLog(context), + appBuilder => appBuilder.UseHttpLogging()); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) @@ -47,48 +63,43 @@ namespace Gameboard.ShogiUI.Sockets app.UseHttpsRedirection(); // Apache handles HTTPS in production. } + UseCorsAndWebSockets(app, allowedOrigins); + app.UseAuthentication(); app.UseAuthorization(); + app.Map("/", () => "OK"); app.MapControllers(); - UseCorsAndWebSockets(app); app.Run(); + + static bool ShouldLog(HttpContext context) + { + var path = context.Request.GetEncodedPathAndQuery(); + + return !path.Contains("swagger") + && !path.Equals("/", StringComparison.Ordinal); + } } - private static void UseCorsAndWebSockets(WebApplication app) + private static void UseCorsAndWebSockets(WebApplication app, string[] allowedOrigins) { + // TODO: Figure out how to make a middleware for sockets? var socketService = app.Services.GetRequiredService(); - var allowedOrigins = app.Configuration.GetSection("Cors:AllowedOrigins").Get(); - var socketOptions = new WebSocketOptions(); foreach (var origin in allowedOrigins) socketOptions.AllowedOrigins.Add(origin); - app.UseCors(options => - { - options - .WithOrigins(allowedOrigins) - .SetIsOriginAllowedToAllowWildcardSubdomains() - .AllowAnyMethod() - .AllowAnyHeader() - .WithExposedHeaders("Set-Cookie") - .AllowCredentials(); - }); + app.UseCors(); app.UseWebSockets(socketOptions); app.Use(async (context, next) => { - Console.WriteLine("Use websocket"); if (context.WebSockets.IsWebSocketRequest) { - Console.WriteLine("Is websocket request"); await socketService.HandleSocketRequest(context); } - else - { - await next(); - } + await next(); }); } @@ -96,48 +107,79 @@ namespace Gameboard.ShogiUI.Sockets { builder.Services.AddHttpLogging(options => { - options.LoggingFields = HttpLoggingFields.Request; + options.LoggingFields = HttpLoggingFields.RequestProperties + | HttpLoggingFields.RequestBody + | HttpLoggingFields.ResponseStatusCode + | HttpLoggingFields.ResponseBody; }); } private static void ConfigureAuthentication(WebApplicationBuilder builder) { - // Add services to the container. - builder.Services - .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + AddJwtAuth(builder); + AddCookieAuth(builder); + SetupAuthSwitch(builder); - builder.Services - .AddAuthentication("CookieOrJwt") - .AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options => - { - options.ForwardDefaultSelector = context => + static void AddJwtAuth(WebApplicationBuilder builder) + { + builder.Services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + } + + static void AddCookieAuth(WebApplicationBuilder builder) + { + builder.Services + .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => { - 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 = SameSiteMode.None; - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - options.SlidingExpiration = true; - }); + options.Cookie.Name = "session-id"; + options.Cookie.SameSite = SameSiteMode.None; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.SlidingExpiration = true; + options.LoginPath = new PathString("/User/LoginAsGuest"); + }); + } + + static void SetupAuthSwitch(WebApplicationBuilder builder) + { + var defaultScheme = "CookieOrJwt"; + builder.Services + .AddAuthentication(defaultScheme) + .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; + }; + }); + builder + .Services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = defaultScheme; + }); + } } private static void ConfigureControllersWithNewtonsoft(WebApplicationBuilder builder) { builder.Services .AddControllers() + //.AddJsonOptions(options => + //{ + // options.AllowInputFormatterExceptionMessages = true; + // options.JsonSerializerOptions.WriteIndented = true; + //}); .AddNewtonsoftJson(options => { options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.ContractResolver = new DefaultContractResolver { - NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true } + NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = false } }; options.SerializerSettings.Converters = new[] { new StringEnumConverter() }; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; @@ -150,7 +192,7 @@ namespace Gameboard.ShogiUI.Sockets { NamingStrategy = new CamelCaseNamingStrategy { - ProcessDictionaryKeys = true + ProcessDictionaryKeys = false } }, Converters = new[] { new StringEnumConverter() }, @@ -161,15 +203,14 @@ namespace Gameboard.ShogiUI.Sockets private static void ConfigureDependencyInjection(WebApplicationBuilder builder) { var services = builder.Services; - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton, JoinByCodeRequestValidator>(); - services.AddSingleton, JoinGameRequestValidator>(); services.AddSingleton(); - services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddHttpClient("couchdb", c => { var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin")); diff --git a/Gameboard.ShogiUI.Sockets/Properties/PublishProfiles/FolderProfile.pubxml b/Shogi.Sockets/Properties/PublishProfiles/FolderProfile.pubxml similarity index 100% rename from Gameboard.ShogiUI.Sockets/Properties/PublishProfiles/FolderProfile.pubxml rename to Shogi.Sockets/Properties/PublishProfiles/FolderProfile.pubxml diff --git a/Gameboard.ShogiUI.Sockets/Properties/ServiceDependencies/local/secrets1.arm.json b/Shogi.Sockets/Properties/ServiceDependencies/local/secrets1.arm.json similarity index 100% rename from Gameboard.ShogiUI.Sockets/Properties/ServiceDependencies/local/secrets1.arm.json rename to Shogi.Sockets/Properties/ServiceDependencies/local/secrets1.arm.json diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Shogi.Sockets/Properties/launchSettings.json similarity index 100% rename from Gameboard.ShogiUI.Sockets/Properties/launchSettings.json rename to Shogi.Sockets/Properties/launchSettings.json diff --git a/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.json b/Shogi.Sockets/Properties/serviceDependencies.json similarity index 100% rename from Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.json rename to Shogi.Sockets/Properties/serviceDependencies.json diff --git a/Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.local.json b/Shogi.Sockets/Properties/serviceDependencies.local.json similarity index 100% rename from Gameboard.ShogiUI.Sockets/Properties/serviceDependencies.local.json rename to Shogi.Sockets/Properties/serviceDependencies.local.json diff --git a/Gameboard.ShogiUI.Sockets/Readme.md b/Shogi.Sockets/Readme.md similarity index 73% rename from Gameboard.ShogiUI.Sockets/Readme.md rename to Shogi.Sockets/Readme.md index cc7e22c..fd3f2fd 100644 --- a/Gameboard.ShogiUI.Sockets/Readme.md +++ b/Shogi.Sockets/Readme.md @@ -1,4 +1,4 @@ -# Gameboard.ShogiUI.Sockets +# Shogi.Sockets # Forgetmenots Don't forget to run `dotnet user-secrets init` within the AAT project. \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs b/Shogi.Sockets/Repositories/CouchModels/BoardStateDocument.cs similarity index 68% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs rename to Shogi.Sockets/Repositories/CouchModels/BoardStateDocument.cs index ba76c72..de40921 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/BoardStateDocument.cs +++ b/Shogi.Sockets/Repositories/CouchModels/BoardStateDocument.cs @@ -1,11 +1,8 @@ using Shogi.Domain; -using System; -using System.Collections.Generic; -using System.Linq; -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { - public class BoardStateDocument : CouchDocument + public class BoardStateDocument : CouchDocument { public string Name { get; set; } @@ -37,16 +34,16 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels public BoardStateDocument(string sessionName, Session shogi) : base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState) { - static Piece MapPiece(Shogi.Domain.Pieces.Piece piece) + static Piece MapPiece(Domain.ValueObjects.Piece piece) { return new Piece { IsPromoted = piece.IsPromoted, Owner = piece.Owner, WhichPiece = piece.WhichPiece }; } Name = sessionName; - Board = shogi.BoardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null ? null : MapPiece(kvp.Value)); + Board = shogi.BoardState.State.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null ? null : MapPiece(kvp.Value)); - Player1Hand = shogi.Player1Hand.Select(piece => MapPiece(piece)).ToArray(); - Player2Hand = shogi.Player2Hand.Select(piece => MapPiece(piece)).ToArray(); + Player1Hand = shogi.BoardState.Player1Hand.Select(piece => MapPiece(piece)).ToArray(); + Player2Hand = shogi.BoardState.Player2Hand.Select(piece => MapPiece(piece)).ToArray(); } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs b/Shogi.Sockets/Repositories/CouchModels/CouchCreatedResult.cs similarity index 79% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs rename to Shogi.Sockets/Repositories/CouchModels/CouchCreatedResult.cs index 3435e7b..e89e93c 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchCreatedResult.cs +++ b/Shogi.Sockets/Repositories/CouchModels/CouchCreatedResult.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { public class CouchCreateResult { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs b/Shogi.Sockets/Repositories/CouchModels/CouchDocument.cs similarity index 92% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs rename to Shogi.Sockets/Repositories/CouchModels/CouchDocument.cs index 29d19c3..95f743d 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchDocument.cs +++ b/Shogi.Sockets/Repositories/CouchModels/CouchDocument.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using System; -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { public abstract class CouchDocument { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs b/Shogi.Sockets/Repositories/CouchModels/CouchFindResult.cs similarity index 77% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs rename to Shogi.Sockets/Repositories/CouchModels/CouchFindResult.cs index daab934..656084d 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchFindResult.cs +++ b/Shogi.Sockets/Repositories/CouchModels/CouchFindResult.cs @@ -1,6 +1,6 @@ using System; -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { internal class CouchFindResult { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs b/Shogi.Sockets/Repositories/CouchModels/CouchViewResult.cs similarity index 87% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs rename to Shogi.Sockets/Repositories/CouchModels/CouchViewResult.cs index ca98d2e..b3a4d5b 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/CouchViewResult.cs +++ b/Shogi.Sockets/Repositories/CouchModels/CouchViewResult.cs @@ -1,6 +1,6 @@ using System; -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { public class CouchViewResult where T : class { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs b/Shogi.Sockets/Repositories/CouchModels/Move.cs similarity index 91% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs rename to Shogi.Sockets/Repositories/CouchModels/Move.cs index 917fdf7..cfa8d1d 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Move.cs +++ b/Shogi.Sockets/Repositories/CouchModels/Move.cs @@ -1,6 +1,6 @@ using Shogi.Domain; -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { public class Move { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs b/Shogi.Sockets/Repositories/CouchModels/Piece.cs similarity index 74% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs rename to Shogi.Sockets/Repositories/CouchModels/Piece.cs index e5df4fc..5e668b5 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/Piece.cs +++ b/Shogi.Sockets/Repositories/CouchModels/Piece.cs @@ -1,6 +1,6 @@ using Shogi.Domain; -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { public class Piece { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs b/Shogi.Sockets/Repositories/CouchModels/SessionDocument.cs similarity index 70% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs rename to Shogi.Sockets/Repositories/CouchModels/SessionDocument.cs index 8b5ac54..e132bf9 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/SessionDocument.cs +++ b/Shogi.Sockets/Repositories/CouchModels/SessionDocument.cs @@ -1,8 +1,8 @@ -using Shogi.Domain; +using Shogi.Contracts.Types; -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { - public class SessionDocument : CouchDocument + public class SessionDocument : CouchDocument { public string Name { get; set; } public string Player1Id { get; set; } @@ -23,9 +23,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels : base(sessionMetaData.Name, WhichDocumentType.Session) { Name = sessionMetaData.Name; - Player1Id = sessionMetaData.Player1; - Player2Id = sessionMetaData.Player2; - IsPrivate = sessionMetaData.IsPrivate; } } } diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs b/Shogi.Sockets/Repositories/CouchModels/UserDocument.cs similarity index 84% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs rename to Shogi.Sockets/Repositories/CouchModels/UserDocument.cs index 940a8bd..a599450 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/UserDocument.cs +++ b/Shogi.Sockets/Repositories/CouchModels/UserDocument.cs @@ -1,6 +1,6 @@ -using Gameboard.ShogiUI.Sockets.Models; +using Shogi.Api.Models; -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { public class UserDocument : CouchDocument { diff --git a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs b/Shogi.Sockets/Repositories/CouchModels/WhichDocumentType.cs similarity index 56% rename from Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs rename to Shogi.Sockets/Repositories/CouchModels/WhichDocumentType.cs index f5a4f72..045101a 100644 --- a/Gameboard.ShogiUI.Sockets/Repositories/CouchModels/WhichDocumentType.cs +++ b/Shogi.Sockets/Repositories/CouchModels/WhichDocumentType.cs @@ -1,4 +1,4 @@ -namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels +namespace Shogi.Api.Repositories.CouchModels { public enum WhichDocumentType { diff --git a/Shogi.Sockets/Repositories/GameboardRepository.cs b/Shogi.Sockets/Repositories/GameboardRepository.cs new file mode 100644 index 0000000..a252100 --- /dev/null +++ b/Shogi.Sockets/Repositories/GameboardRepository.cs @@ -0,0 +1,232 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shogi.Contracts.Types; +using Shogi.Api.Repositories.CouchModels; +using System.Collections.ObjectModel; +using System.Text; +using Session = Shogi.Domain.Session; + +namespace Shogi.Api.Repositories +{ + public interface IGameboardRepository + { + Task CreateBoardState(Session session); + Task CreateUser(Models.User user); + Task> ReadSessionMetadatas(); + Task ReadSession(string name); + Task UpdateSession(SessionMetadata session); + Task ReadSessionMetaData(string name); + Task ReadUser(string userName); + } + + public class GameboardRepository + { + /// + /// Returns session, board state, and user documents, grouped by session. + /// + private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate"; + /// + /// Returns session and user documents, grouped by session. + /// + private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata"; + private static readonly string View_User = "_design/user/_view/user"; + private const string ApplicationJson = "application/json"; + private readonly HttpClient client; + private readonly ILogger logger; + + public GameboardRepository(IHttpClientFactory clientFactory, ILogger logger) + { + client = clientFactory.CreateClient("couchdb"); + this.logger = logger; + } + + //public async Task> ReadSessionMetadatas() + //{ + // var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString(); + // var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); + // var responseContent = await response.Content.ReadAsStringAsync(); + // var result = JsonConvert.DeserializeObject>(responseContent); + // if (result != null) + // { + // var groupedBySession = result.rows.GroupBy(row => row.id); + // var sessions = new List(result.total_rows / 3); + // foreach (var group in groupedBySession) + // { + // /** + // * A group contains 3 elements. + // * 1) The session metadata. + // * 2) User document of Player1. + // * 3) User document of Player2. + // */ + // var session = group.FirstOrDefault()?.doc.ToObject(); + // var player1 = group.Skip(1).FirstOrDefault()?.doc.ToObject(); + // var player2Doc = group.Skip(2).FirstOrDefault()?.doc; + // if (session != null && player1 != null && player2Doc != null) + // { + // var player2 = IsUserDocument(player2Doc) + // ? new Models.User(player2Doc.ToObject()!) + // : null; + // //sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id)); + // } + // } + // return new Collection(sessions); + // } + // return new Collection(Array.Empty()); + //} + + private static bool IsUserDocument(JObject player2Doc) + { + return player2Doc?.SelectToken(nameof(CouchDocument.DocumentType))?.Value() == WhichDocumentType.User; + } + + //public async Task ReadSession(string name) + //{ + // static Domain.ValueObjects.Piece? MapPiece(Piece? piece) + // { + // return piece == null + // ? null + // : Domain.ValueObjects.Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); + // } + + // var queryParams = new QueryBuilder + // { + // { "include_docs", "true" }, + // { "startkey", JsonConvert.SerializeObject(new [] {name}) }, + // { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } + // }.ToQueryString(); + // var query = $"{View_SessionWithBoardState}{queryParams}"; + // logger.LogInformation("ReadSession() query: {query}", query); + // var response = await client.GetAsync(query); + // var responseContent = await response.Content.ReadAsStringAsync(); + // var result = JsonConvert.DeserializeObject>(responseContent); + // if (result != null && result.rows.Length > 2) + // { + // var group = result.rows; + // /** + // * A group contains multiple elements. + // * 0) The session metadata. + // * 1) User documents of Player1. + // * 2) User documents of Player1. + // * 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :( + // * Everything Else) Snapshots of the boardstate after every player move. + // */ + // var session = group[0].doc.ToObject(); + // var player1 = group[1].doc.ToObject(); + // var player2Doc = group[2].doc; + // var boardState = group.Last().doc.ToObject(); + + // if (session != null && player1 != null && boardState != null) + // { + // var player2 = IsUserDocument(player2Doc) + // ? new Models.User(player2Doc.ToObject()!) + // : null; + // var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id); + // var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value))); + // //return new Session(shogiBoardState, metaData); + // } + // } + // return null; + //} + + //public async Task ReadSessionMetaData(string name) + //{ + // var queryParams = new QueryBuilder + // { + // { "include_docs", "true" }, + // { "startkey", JsonConvert.SerializeObject(new [] {name}) }, + // { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } + // }.ToQueryString(); + // var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); + // var responseContent = await response.Content.ReadAsStringAsync(); + // var result = JsonConvert.DeserializeObject>(responseContent); + // if (result != null && result.rows.Length > 2) + // { + // var group = result.rows; + // /** + // * A group contains 3 elements. + // * 1) The session metadata. + // * 2) User document of Player1. + // * 3) User document of Player2. + // */ + // var session = group[0].doc.ToObject(); + // var player1 = group[1].doc.ToObject(); + // var player2Doc = group[2].doc; + // if (session != null && player1 != null) + // { + // var player2 = IsUserDocument(player2Doc) + // ? new Models.User(player2Doc.ToObject()!) + // : null; + // return new SessionMetadata(session.Name, session.IsPrivate, player1.Id, player2?.Id); + // } + // } + // return null; + //} + + /// + /// Saves a snapshot of board state and the most recent move. + /// + public async Task CreateBoardState(Session session) + { + var boardStateDocument = new BoardStateDocument(session.Name, session); + var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); + var response = await client.PostAsync(string.Empty, content); + response.EnsureSuccessStatusCode(); + } + + //public async Task CreateSession(SessionMetadata session) + //{ + // var sessionDocument = new SessionDocument(session); + // var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson); + // var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent); + + // var boardStateDocument = new BoardStateDocument(session.Name, new Session()); + // 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 UpdateSession(SessionMetadata session) + //{ + // // GET existing session to get revisionId. + // var readResponse = await client.GetAsync(session.Name); + // readResponse.EnsureSuccessStatusCode(); + // var sessionDocument = JsonConvert.DeserializeObject(await readResponse.Content.ReadAsStringAsync()); + + // // PUT the document with the revisionId. + // var couchModel = new SessionDocument(session) + // { + // RevisionId = sessionDocument?.RevisionId + // }; + // var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); + // var response = await client.PutAsync(couchModel.Id, content); + // response.EnsureSuccessStatusCode(); + //} + + //public async Task PutJoinPublicSession(PutJoinPublicSession request) + //{ + // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + // var response = await client.PutAsync(JoinSessionRoute, content); + // var json = await response.Content.ReadAsStringAsync(); + // return JsonConvert.DeserializeObject(json).JoinSucceeded; + //} + + //public async Task PostJoinPrivateSession(PostJoinPrivateSession request) + //{ + // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); + // var response = await client.PostAsync(JoinSessionRoute, content); + // var json = await response.Content.ReadAsStringAsync(); + // var deserialized = JsonConvert.DeserializeObject(json); + // if (deserialized.JoinSucceeded) + // { + // return deserialized.SessionName; + // } + // return null; + //} + } +} diff --git a/Shogi.Sockets/Repositories/QueryRepository.cs b/Shogi.Sockets/Repositories/QueryRepository.cs new file mode 100644 index 0000000..14d0eaa --- /dev/null +++ b/Shogi.Sockets/Repositories/QueryRepository.cs @@ -0,0 +1,28 @@ +using Dapper; +using Shogi.Contracts.Types; +using System.Data.SqlClient; + +namespace Shogi.Api.Repositories; + +public class QueryRepository : IQueryRespository +{ + private readonly string connectionString; + + public QueryRepository(IConfiguration configuration) + { + connectionString = configuration.GetConnectionString("ShogiDatabase"); + } + + public async Task> ReadAllSessionsMetadata() + { + using var connection = new SqlConnection(connectionString); + return await connection.QueryAsync( + "session.ReadAllSessionsMetadata", + commandType: System.Data.CommandType.StoredProcedure); + } +} + +public interface IQueryRespository +{ + Task> ReadAllSessionsMetadata(); +} \ No newline at end of file diff --git a/Shogi.Sockets/Repositories/SessionRepository.cs b/Shogi.Sockets/Repositories/SessionRepository.cs new file mode 100644 index 0000000..bf3570e --- /dev/null +++ b/Shogi.Sockets/Repositories/SessionRepository.cs @@ -0,0 +1,37 @@ +using Dapper; +using Shogi.Domain; +using System.Data; +using System.Data.SqlClient; +using System.Text.Json; + +namespace Shogi.Api.Repositories; + +public class SessionRepository : ISessionRepository +{ + private readonly string connectionString; + + public SessionRepository(IConfiguration configuration) + { + connectionString = configuration.GetConnectionString("ShogiDatabase"); + } + + public async Task CreateSession(Session session) + { + var initialBoardState = JsonSerializer.Serialize(session.BoardState); + using var connection = new SqlConnection(connectionString); + await connection.ExecuteAsync( + "session.CreateSession", + new + { + SessionName = session.Name, + Player1Name = session.Player1, + InitialBoardStateDocument = initialBoardState + }, + commandType: CommandType.StoredProcedure); + } +} + +public interface ISessionRepository +{ + Task CreateSession(Session session); +} \ No newline at end of file diff --git a/Shogi.Sockets/Repositories/UserRepository.cs b/Shogi.Sockets/Repositories/UserRepository.cs new file mode 100644 index 0000000..1980739 --- /dev/null +++ b/Shogi.Sockets/Repositories/UserRepository.cs @@ -0,0 +1,47 @@ +using Dapper; +using Shogi.Api.Models; +using System.Data; +using System.Data.SqlClient; + +namespace Shogi.Api.Repositories; + +public class UserRepository : IUserRepository +{ + private readonly string connectionString; + + public UserRepository(IConfiguration configuration) + { + connectionString = configuration.GetConnectionString("ShogiDatabase"); + } + + public async Task CreateUser(User user) + { + using var connection = new SqlConnection(connectionString); + await connection.ExecuteAsync( + "user.CreateUser", + new + { + Name = user.Id, + DisplayName = user.DisplayName, + Platform = user.LoginPlatform.ToString() + }, + commandType: CommandType.StoredProcedure); + } + + public async Task ReadUser(string id) + { + using var connection = new SqlConnection(connectionString); + var results = await connection.QueryAsync( + "user.ReadUser", + new { Name = id }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } +} + +public interface IUserRepository +{ + Task CreateUser(User user); + Task ReadUser(string id); +} \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs b/Shogi.Sockets/Services/SocketService.cs similarity index 59% rename from Gameboard.ShogiUI.Sockets/Services/SocketService.cs rename to Shogi.Sockets/Services/SocketService.cs index fefbd52..a32dedc 100644 --- a/Gameboard.ShogiUI.Sockets/Services/SocketService.cs +++ b/Shogi.Sockets/Services/SocketService.cs @@ -1,22 +1,16 @@ 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 Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System; -using System.Linq; +using Shogi.Contracts.Socket; +using Shogi.Contracts.Types; +using Shogi.Api.Extensions; +using Shogi.Api.Managers; +using Shogi.Api.Repositories; using System.Net; using System.Net.WebSockets; -using System.Threading.Tasks; -namespace Gameboard.ShogiUI.Sockets.Services +namespace Shogi.Api.Services { - public interface ISocketService + public interface ISocketService { Task HandleSocketRequest(HttpContext context); } @@ -28,32 +22,16 @@ namespace Gameboard.ShogiUI.Sockets.Services { private readonly ILogger logger; private readonly ISocketConnectionManager communicationManager; - private readonly IGameboardRepository gameboardRepository; - private readonly IGameboardManager gameboardManager; private readonly ISocketTokenCache tokenManager; - private readonly IJoinByCodeHandler joinByCodeHandler; - private readonly IValidator joinByCodeRequestValidator; - private readonly IValidator joinGameRequestValidator; public SocketService( ILogger logger, ISocketConnectionManager communicationManager, - IGameboardRepository gameboardRepository, - IGameboardManager gameboardManager, - ISocketTokenCache tokenManager, - IJoinByCodeHandler joinByCodeHandler, - IValidator joinByCodeRequestValidator, - IValidator joinGameRequestValidator - ) : base() + ISocketTokenCache tokenManager) : 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) @@ -81,23 +59,14 @@ namespace Gameboard.ShogiUI.Sockets.Services var message = await socket.ReceiveTextAsync(); if (string.IsNullOrWhiteSpace(message)) continue; logger.LogInformation("Request \n{0}\n", message); - var request = JsonConvert.DeserializeObject(message); - if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action)) + var request = JsonConvert.DeserializeObject(message); + if (request == null || !Enum.IsDefined(typeof(SocketAction), request.Action)) { await socket.SendTextAsync("Error: Action not recognized."); continue; } switch (request.Action) { - case ClientAction.JoinByCode: - { - var req = JsonConvert.DeserializeObject(message); - if (req != null && await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) - { - await joinByCodeHandler.Handle(req, userName); - } - break; - } default: await socket.SendTextAsync($"Received your message with action {request.Action}, but did no work."); break; diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Shogi.Sockets/Shogi.Api.csproj similarity index 69% rename from Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj rename to Shogi.Sockets/Shogi.Api.csproj index cb5ed71..0566d83 100644 --- a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj +++ b/Shogi.Sockets/Shogi.Api.csproj @@ -12,26 +12,24 @@ - - - - - - - - + + + + + + + + + - + + - + - - - - diff --git a/Shogi.Sockets/ShogiUserClaimsTransformer.cs b/Shogi.Sockets/ShogiUserClaimsTransformer.cs new file mode 100644 index 0000000..df757e7 --- /dev/null +++ b/Shogi.Sockets/ShogiUserClaimsTransformer.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; +using Shogi.Api.Extensions; +using Shogi.Api.Models; +using Shogi.Api.Repositories; +using System.Security.Claims; + +namespace Shogi.Api; + +/// +/// Standardizes the claims from third party issuers. Also registers new msal users in the database. +/// +public class ShogiUserClaimsTransformer : IShogiUserClaimsTransformer +{ + private readonly IUserRepository userRepository; + + public ShogiUserClaimsTransformer(IUserRepository userRepository) + { + this.userRepository = userRepository; + } + + public async Task TransformAsync(ClaimsPrincipal principal) + { + var newPrincipal = principal.IsMicrosoft() + ? await CreateClaimsFromMicrosoftPrincipal(principal) + : await CreateClaimsFromGuestPrincipal(principal); + + return newPrincipal; + } + + public async Task CreateClaimsFromGuestPrincipal(ClaimsPrincipal principal) + { + var id = principal.GetGuestUserId(); + if (string.IsNullOrWhiteSpace(id)) + { + var newUser = User.CreateGuestUser(Guid.NewGuid().ToString()); + await this.userRepository.CreateUser(newUser); + return new ClaimsPrincipal(CreateClaimsIdentity(newUser)); + } + + var user = await this.userRepository.ReadUser(id); + if (user == null) throw new UnauthorizedAccessException("Guest account does not exist."); + return new ClaimsPrincipal(CreateClaimsIdentity(user)); + } + + private async Task CreateClaimsFromMicrosoftPrincipal(ClaimsPrincipal principal) + { + var id = principal.GetMsalAccountId(); + if (string.IsNullOrWhiteSpace(id)) + { + throw new UnauthorizedAccessException("Found MSAL claims but no preferred_username."); + } + + var user = await this.userRepository.ReadUser(id); + if (user == null) + { + user = User.CreateMsalUser(id); + await this.userRepository.CreateUser(user); + } + return new ClaimsPrincipal(CreateClaimsIdentity(user)); + + } + + private static ClaimsIdentity CreateClaimsIdentity(User user) + { + var claims = new List(4) + { + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.DisplayName), + }; + if (user.LoginPlatform == WhichLoginPlatform.Guest) + { + + claims.Add(new Claim(ClaimTypes.Role, "Guest")); + return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + } + else + { + return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme); + } + } +} + +public interface IShogiUserClaimsTransformer : IClaimsTransformation +{ + Task CreateClaimsFromGuestPrincipal(ClaimsPrincipal principal); +} \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/appsettings.Development.json b/Shogi.Sockets/appsettings.Development.json similarity index 100% rename from Gameboard.ShogiUI.Sockets/appsettings.Development.json rename to Shogi.Sockets/appsettings.Development.json diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Shogi.Sockets/appsettings.json similarity index 60% rename from Gameboard.ShogiUI.Sockets/appsettings.json rename to Shogi.Sockets/appsettings.json index 87c4844..5180544 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Shogi.Sockets/appsettings.json @@ -2,14 +2,19 @@ "AppSettings": { "CouchDB": { "Database": "shogi-dev", - "Url": "http://192.168.1.15:5984" + "Url": "http://192.168.1.177:5984" } }, + "ConnectionStrings": { + "ShogiDatabase": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Shogi;Integrated Security=True;Application Name=Shogi.Sockets" + }, "Logging": { "LogLevel": { "Default": "Warning", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Error" + "Microsoft.Hosting.Lifetime": "Error", + "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information", + "System.Net.Http.HttpClient": "Error" } }, "AzureAd": { @@ -25,6 +30,5 @@ "https://api.lucaserver.space", "https://lucaserver.space" ] - }, - "AllowedHosts": "*" + } } \ No newline at end of file diff --git a/Shogi.UI/.config/dotnet-tools.json b/Shogi.UI/.config/dotnet-tools.json new file mode 100644 index 0000000..4b64fb7 --- /dev/null +++ b/Shogi.UI/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "microsoft.dotnet-msidentity": { + "version": "1.0.5", + "commands": [ + "dotnet-msidentity" + ] + } + } +} \ No newline at end of file diff --git a/Shogi.UI/App.razor b/Shogi.UI/App.razor new file mode 100644 index 0000000..b6c75d4 --- /dev/null +++ b/Shogi.UI/App.razor @@ -0,0 +1,31 @@ +@**@ + + + + + @* + + Authorizing!! + + + @if (context.User.Identity?.IsAuthenticated != true) + { + + } + else + { +

You are not authorized to access this resource.

+ } +
+
*@ + +
+ + Not found + +

Sorry, there's nothing at this address.

+
+
+
+ +@*
*@ diff --git a/Shogi.UI/Pages/Authentication.razor b/Shogi.UI/Pages/Authentication.razor new file mode 100644 index 0000000..d1b6cf9 --- /dev/null +++ b/Shogi.UI/Pages/Authentication.razor @@ -0,0 +1,7 @@ +@page "/authentication/{action}" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@**@ + +@code{ + [Parameter] public string? Action { get; set; } +} diff --git a/Shogi.UI/Pages/Home/Account/AccountManager.cs b/Shogi.UI/Pages/Home/Account/AccountManager.cs new file mode 100644 index 0000000..a6c9afd --- /dev/null +++ b/Shogi.UI/Pages/Home/Account/AccountManager.cs @@ -0,0 +1,138 @@ +using Microsoft.AspNetCore.Components; +using Shogi.UI.Pages.Home.Api; +using Shogi.UI.Shared; + +namespace Shogi.UI.Pages.Home.Account; + +public class AccountManager +{ + private readonly AccountState accountState; + private readonly IShogiApi shogiApi; + private readonly IConfiguration configuration; + private readonly ILocalStorage localStorage; + //private readonly AuthenticationStateProvider authState; + private readonly NavigationManager navigation; + private readonly ShogiSocket shogiSocket; + + public AccountManager( + AccountState accountState, + IShogiApi unauthenticatedClient, + IConfiguration configuration, + //AuthenticationStateProvider authState, + ILocalStorage localStorage, + NavigationManager navigation, + ShogiSocket shogiSocket) + { + this.accountState = accountState; + this.shogiApi = unauthenticatedClient; + this.configuration = configuration; + //this.authState = authState; + this.localStorage = localStorage; + this.navigation = navigation; + this.shogiSocket = shogiSocket; + } + + private User? User { get => accountState.User; set => accountState.User = value; } + + public async Task LoginWithGuestAccount() + { + var response = await shogiApi.GetGuestToken(); + if (response != null) + { + User = new User + { + DisplayName = response.DisplayName, + Id = response.UserId, + OneTimeSocketToken = response.OneTimeToken, + WhichAccountPlatform = WhichAccountPlatform.Guest + }; + await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString()); + await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest); + } + } + + public async Task LoginWithMicrosoftAccount() + { + throw new NotImplementedException(); + //var state = await authState.GetAuthenticationStateAsync(); + + //if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated != true) + //{ + // navigation.NavigateTo("authentication/login"); + // return; + //} + + //var id = state.User.Identity.Name; + //var socketToken = await shogiApi.GetToken(); + //if (socketToken.HasValue) + //{ + // User = new User + // { + // DisplayName = id, + // Id = id, + // OneTimeSocketToken = socketToken.Value + // }; + + // await ConnectToSocketAsync(); + // await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft); + // } + } + + /// + /// Try to log in with the account used from the previous browser session. + /// + /// + public async Task TryLoginSilentAsync() + { + var platform = await localStorage.GetAccountPlatform(); + if (platform == WhichAccountPlatform.Guest) + { + var response = await shogiApi.GetGuestToken(); + if (response != null) + { + User = new User + { + DisplayName = response.DisplayName, + Id = response.UserId, + OneTimeSocketToken = response.OneTimeToken, + WhichAccountPlatform = WhichAccountPlatform.Guest + }; + } + } + else if (platform == WhichAccountPlatform.Microsoft) + { + Console.WriteLine("Login Microsoft"); + throw new NotImplementedException(); + //var state = await authState.GetAuthenticationStateAsync(); + //if (state.User?.Identity?.Name != null) + //{ + // var id = state.User.Identity; + // User = new User + // { + // DisplayName = id.Name, + // Id = id.Name + // }; + // var token = await shogiApi.GetToken(); + // if (token.HasValue) + // { + // User.OneTimeSocketToken = token.Value; + // } + //} + // TODO: If this fails then platform saved to localStorage should get cleared + } + + if (User != null) + { + await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString()); + return true; + } + + return false; + } + + public async Task LogoutAsync() + { + await Task.WhenAll(shogiApi.GuestLogout(), localStorage.DeleteAccountPlatform()); + User = null; + } +} diff --git a/Shogi.UI/Pages/Home/Account/AccountState.cs b/Shogi.UI/Pages/Home/Account/AccountState.cs new file mode 100644 index 0000000..6abcf85 --- /dev/null +++ b/Shogi.UI/Pages/Home/Account/AccountState.cs @@ -0,0 +1,28 @@ +namespace Shogi.UI.Pages.Home.Account; + +public class AccountState +{ + public event EventHandler? LoginChangedEvent; + + private User? user; + public User? User + { + get => user; + set + { + if (user != value) + { + user = value; + EmitLoginChangedEvent(); + } + } + } + + private void EmitLoginChangedEvent() + { + LoginChangedEvent?.Invoke(this, new LoginEventArgs + { + User = User + }); + } +} diff --git a/Shogi.UI/Pages/Home/Account/LocalStorageExtensions.cs b/Shogi.UI/Pages/Home/Account/LocalStorageExtensions.cs new file mode 100644 index 0000000..fe76196 --- /dev/null +++ b/Shogi.UI/Pages/Home/Account/LocalStorageExtensions.cs @@ -0,0 +1,24 @@ +using Shogi.UI.Shared; + +namespace Shogi.UI.Pages.Home.Account +{ + public static class LocalStorageExtensions + { + private const string AccountPlatform = "AccountPlatform"; + + public static Task GetAccountPlatform(this ILocalStorage self) + { + return self.Get(AccountPlatform).AsTask(); + } + + public static Task SetAccountPlatform(this ILocalStorage self, WhichAccountPlatform platform) + { + return self.Set(AccountPlatform, platform.ToString()).AsTask(); + } + + public static Task DeleteAccountPlatform(this ILocalStorage self) + { + return self.Delete(AccountPlatform).AsTask(); + } + } +} diff --git a/Shogi.UI/Pages/Home/Account/LoginEventArgs.cs b/Shogi.UI/Pages/Home/Account/LoginEventArgs.cs new file mode 100644 index 0000000..865e387 --- /dev/null +++ b/Shogi.UI/Pages/Home/Account/LoginEventArgs.cs @@ -0,0 +1,7 @@ +namespace Shogi.UI.Pages.Home.Account +{ + public class LoginEventArgs : EventArgs + { + public User? User { get; set; } + } +} diff --git a/Shogi.UI/Pages/Home/Account/User.cs b/Shogi.UI/Pages/Home/Account/User.cs new file mode 100644 index 0000000..d91626d --- /dev/null +++ b/Shogi.UI/Pages/Home/Account/User.cs @@ -0,0 +1,12 @@ +namespace Shogi.UI.Pages.Home.Account +{ + public class User + { + public string Id { get; set; } + public string DisplayName { get; set; } + + public WhichAccountPlatform WhichAccountPlatform { get; set; } + + public Guid OneTimeSocketToken { get; set; } + } +} diff --git a/Shogi.UI/Pages/Home/Account/WhichAccountPlatform.cs b/Shogi.UI/Pages/Home/Account/WhichAccountPlatform.cs new file mode 100644 index 0000000..04d2bc8 --- /dev/null +++ b/Shogi.UI/Pages/Home/Account/WhichAccountPlatform.cs @@ -0,0 +1,8 @@ +namespace Shogi.UI.Pages.Home.Account +{ + public enum WhichAccountPlatform + { + Guest, + Microsoft + } +} diff --git a/Shogi.UI/Pages/Home/Api/CookieMessageHandler.cs b/Shogi.UI/Pages/Home/Api/CookieMessageHandler.cs new file mode 100644 index 0000000..2aac297 --- /dev/null +++ b/Shogi.UI/Pages/Home/Api/CookieMessageHandler.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Http; + +namespace Shogi.UI.Pages.Home.Api +{ + public class CookieCredentialsMessageHandler : DelegatingHandler + { + + public CookieCredentialsMessageHandler() + { + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); + return base.SendAsync(request, cancellationToken); + } + } +} diff --git a/Shogi.UI/Pages/Home/Api/IShogiApi.cs b/Shogi.UI/Pages/Home/Api/IShogiApi.cs new file mode 100644 index 0000000..e7bb155 --- /dev/null +++ b/Shogi.UI/Pages/Home/Api/IShogiApi.cs @@ -0,0 +1,17 @@ +using Shogi.Contracts.Api; +using Shogi.Contracts.Types; +using System.Net; + +namespace Shogi.UI.Pages.Home.Api +{ + public interface IShogiApi + { + Task GetGuestToken(); + Task GetSession(string name); + Task GetSessions(); + Task GetToken(); + Task GuestLogout(); + Task PostMove(string sessionName, Move move); + Task PostSession(string name, bool isPrivate); + } +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Api/MsalMessageHandler.cs b/Shogi.UI/Pages/Home/Api/MsalMessageHandler.cs new file mode 100644 index 0000000..d91861c --- /dev/null +++ b/Shogi.UI/Pages/Home/Api/MsalMessageHandler.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +namespace Shogi.UI.Pages.Home.Api +{ + public class MsalMessageHandler : AuthorizationMessageHandler + { + public MsalMessageHandler(IAccessTokenProvider provider, NavigationManager navigation) : base(provider, navigation) + { + ConfigureHandler( + authorizedUrls: new[] { "https://api.lucaserver.space", "https://localhost:5001" }, + scopes: new[] { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/DefaultScope" }, + returnUrl: "https://localhost:3000"); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return base.SendAsync(request, cancellationToken); + } + } +} diff --git a/Shogi.UI/Pages/Home/Api/ShogiApi.cs b/Shogi.UI/Pages/Home/Api/ShogiApi.cs new file mode 100644 index 0000000..92abe7e --- /dev/null +++ b/Shogi.UI/Pages/Home/Api/ShogiApi.cs @@ -0,0 +1,98 @@ +using Shogi.Contracts.Api; +using Shogi.Contracts.Types; +using Shogi.UI.Pages.Home.Account; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Shogi.UI.Pages.Home.Api +{ + public class ShogiApi : IShogiApi + { + public const string GuestClientName = "Guest"; + public const string MsalClientName = "Msal"; + public const string AnonymouseClientName = "Anonymous"; + + private readonly JsonSerializerOptions serializerOptions; + private readonly IHttpClientFactory clientFactory; + private readonly AccountState accountState; + + public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState) + { + serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }; + serializerOptions.Converters.Add(new JsonStringEnumConverter()); + this.clientFactory = clientFactory; + this.accountState = accountState; + } + + private HttpClient HttpClient => accountState.User?.WhichAccountPlatform switch + { + WhichAccountPlatform.Guest => clientFactory.CreateClient(GuestClientName), + WhichAccountPlatform.Microsoft => clientFactory.CreateClient(MsalClientName), + _ => clientFactory.CreateClient(AnonymouseClientName) + }; + + public async Task GetGuestToken() + { + var response = await HttpClient.GetAsync(new Uri("User/GuestToken", UriKind.Relative)); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(serializerOptions); + } + return null; + } + + public async Task GuestLogout() + { + var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null); + response.EnsureSuccessStatusCode(); + } + + public async Task GetSession(string name) + { + var response = await HttpClient.GetAsync(new Uri($"Session/{name}", UriKind.Relative)); + if (response.IsSuccessStatusCode) + { + return (await response.Content.ReadFromJsonAsync(serializerOptions))?.Session; + } + return null; + } + + public async Task GetSessions() + { + var response = await HttpClient.GetAsync(new Uri("Session", UriKind.Relative)); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(serializerOptions); + } + return null; + } + + public async Task GetToken() + { + var response = await HttpClient.GetAsync(new Uri("User/Token", UriKind.Relative)); + var deserialized = await response.Content.ReadFromJsonAsync(serializerOptions); + return deserialized?.OneTimeToken; + } + + public async Task PostMove(string sessionName, Contracts.Types.Move move) + { + await this.HttpClient.PostAsJsonAsync($"{sessionName}/Move", new MovePieceCommand { Move = move }); + } + + public async Task PostSession(string name, bool isPrivate) + { + var response = await HttpClient.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand + { + Name = name, + IsPrivate = isPrivate + }); + return response.StatusCode; + } + } +} diff --git a/Shogi.UI/Pages/Home/GameBoard.razor b/Shogi.UI/Pages/Home/GameBoard.razor new file mode 100644 index 0000000..0819111 --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBoard.razor @@ -0,0 +1,62 @@ +@using Shogi.Contracts.Types; +@inject IShogiApi ShogiApi +@inject AccountState Account; + +
+ @for (var rank = 9; rank > 0; rank--) + { + foreach (var file in Files) + { + var position = $"{file}{rank}"; + var piece = session?.BoardState.Board[position]; +
+ +
+ } + } +
+ 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 +
+
+ A + B + C + D + E + F + G + H + I +
+
+ +@code { + [Parameter] + public string? SessionName { get; set; } + static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" }; + WhichPlayer Perspective => Account.User?.Id == session?.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2; + + Session? session; + string? selectedPosition; + + protected override async Task OnParametersSetAsync() + { + if (!string.IsNullOrWhiteSpace(SessionName)) + { + this.session = await ShogiApi.GetSession(SessionName); + } + } + + void OnClickTile(Piece? piece, string position) + { + + } +} diff --git a/Shogi.UI/Pages/Home/GameBoard.razor.css b/Shogi.UI/Pages/Home/GameBoard.razor.css new file mode 100644 index 0000000..24f4769 --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBoard.razor.css @@ -0,0 +1,63 @@ +.game-board { + background-color: #444; + display: grid; + grid-template-areas: + "rank A9 B9 C9 D9 E9 F9 G9 H9 I9" + "rank A8 B8 C8 D8 E8 F8 G8 H8 I8" + "rank A7 B7 C7 D7 E7 F7 G7 H7 I7" + "rank A6 B6 C6 D6 E6 F6 G6 H6 I6" + "rank A5 B5 C5 D5 E5 F5 G5 H5 I5" + "rank A4 B4 C4 D4 E4 F4 G4 H4 I4" + "rank A3 B3 C3 D3 E3 F3 G3 H3 I3" + "rank A2 B2 C2 D2 E2 F2 G2 H2 I2" + "rank A1 B1 C1 D1 E1 F1 G1 H1 I1" + ". file file file file file file file file file"; + grid-template-columns: auto repeat(9, minmax(0, 1fr)); + grid-template-rows: repeat(9, minmax(0, 1fr)) auto; + max-height: calc(100vh - 3rem); + width: calc(100vmin * 0.9167); + aspect-ratio: 0.9167; + gap: 3px; +} + + .game-board[data-perspective="Player2"] { + grid-template-areas: + "file file file file file file file file file ." + "I1 H1 G1 F1 E1 D1 C1 B1 A1 rank" + "I2 H2 G2 F2 E2 D2 C2 B2 A2 rank" + "I3 H3 G3 F3 E3 D3 C3 B3 A3 rank" + "I4 H4 G4 F4 E4 D4 C4 B4 A4 rank" + "I5 H5 G5 F5 E5 D5 C5 B5 A5 rank" + "I6 H6 G6 F6 E6 D6 C6 B6 A6 rank" + "I7 H7 G7 F7 E7 D7 C7 B7 A7 rank" + "I8 H8 G8 F8 E8 D8 C8 B8 A8 rank" + "I9 H9 G9 F9 E9 D9 C9 B9 A9 rank"; + grid-template-columns: repeat(9, minmax(0, 1fr)) auto; + grid-template-rows: auto repeat(9, minmax(0, 1fr)); + } + +.tile { + background-color: beige; + display: grid; + place-content: center; + padding: 0.25rem; +} + +.ruler { + color: beige; + display: flex; + flex-direction: row; + justify-content: space-around; +} + + .ruler.vertical { + flex-direction: column; + } + +.game-board[data-perspective="Player2"] .ruler { + flex-direction: row-reverse; +} + + .game-board[data-perspective="Player2"] .ruler.vertical { + flex-direction: column-reverse; + } diff --git a/Shogi.UI/Pages/Home/GameBrowser.razor b/Shogi.UI/Pages/Home/GameBrowser.razor new file mode 100644 index 0000000..d5220ef --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBrowser.razor @@ -0,0 +1,109 @@ +@using Shogi.Contracts.Types +@using System.ComponentModel.DataAnnotations +@using System.Net +@inject IShogiApi ShogiApi; +@inject ShogiSocket ShogiSocket; + +
+ + +
+
+
+ @if (!sessions.Any()) + { +

No games exist

+ } + @foreach (var session in sessions) + { + + } +
+
+
+ + + +

Start a new session

+
+ + +
+
+
+ + +
+ +
+ + @if (createSessionStatusCode == HttpStatusCode.Created) + { + + } + else if (createSessionStatusCode == HttpStatusCode.Conflict) + { + + } + +
+
+
+
+ +@code { + [Parameter] + public Action? ActiveSessionChanged { get; set; } + CreateForm createForm = new(); + SessionMetadata[] sessions = Array.Empty(); + SessionMetadata? activeSession; + HttpStatusCode? createSessionStatusCode; + + protected override async Task OnInitializedAsync() + { + ShogiSocket.OnCreateGameMessage += async (sender, message) => await FetchSessions(); + await FetchSessions(); + } + string ActiveCss(SessionMetadata s) => s == activeSession ? "active" : string.Empty; + + void OnClickSession(SessionMetadata s) + { + activeSession = s; + ActiveSessionChanged?.Invoke(s); + } + + async Task FetchSessions() + { + var sessions = await ShogiApi.GetSessions(); + if (sessions != null) + { + this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray(); + } + } + + async Task CreateSession() + { + createSessionStatusCode = await ShogiApi.PostSession(createForm.Name, createForm.IsPrivate); + } + + private class CreateForm + { + [Required] + public string Name { get; set; } = string.Empty; + public bool IsPrivate { get; set; } + } +} diff --git a/Shogi.UI/Pages/Home/GameBrowser.razor.css b/Shogi.UI/Pages/Home/GameBrowser.razor.css new file mode 100644 index 0000000..0baeedb --- /dev/null +++ b/Shogi.UI/Pages/Home/GameBrowser.razor.css @@ -0,0 +1,13 @@ +.game-browser { + padding: 0.5rem; + background-color: var(--contrast-color); +} + +#search-pane button.list-group-item { + display: grid; + grid-template-columns: 1fr auto; +} +#search-pane button.list-group-item.active { + background-color: #444; + border-color: #666; +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/GamePiece.razor b/Shogi.UI/Pages/Home/GamePiece.razor new file mode 100644 index 0000000..3f22394 --- /dev/null +++ b/Shogi.UI/Pages/Home/GamePiece.razor @@ -0,0 +1,44 @@ +@using Shogi.Contracts.Types + +
+ @switch (Piece?.WhichPiece) + { + case WhichPiece.Bishop: + + break; + case WhichPiece.GoldGeneral: + + break; + case WhichPiece.King: + + break; + case WhichPiece.Knight: + + break; + case WhichPiece.Lance: + + break; + case WhichPiece.Pawn: + + break; + case WhichPiece.Rook: + + break; + case WhichPiece.SilverGeneral: + + break; + default: + @*render nothing*@ + break; + } +
+ +@code { + [Parameter] + public Contracts.Types.Piece? Piece { get; set; } + + [Parameter] + public WhichPlayer Perspective { get; set; } + + private bool IsPromoted => Piece != null && Piece.IsPromoted; +} diff --git a/Shogi.UI/Pages/Home/GamePiece.razor.css b/Shogi.UI/Pages/Home/GamePiece.razor.css new file mode 100644 index 0000000..0dae5fc --- /dev/null +++ b/Shogi.UI/Pages/Home/GamePiece.razor.css @@ -0,0 +1,8 @@ +::deep svg { + max-width: 100%; + max-height: 100%; +} + +[data-upsidedown] { + transform: rotateZ(180deg); +} diff --git a/Shogi.UI/Pages/Home/Home.razor b/Shogi.UI/Pages/Home/Home.razor new file mode 100644 index 0000000..176acbd --- /dev/null +++ b/Shogi.UI/Pages/Home/Home.razor @@ -0,0 +1,46 @@ +@page "/" +@using Shogi.Contracts.Types +@using System.Net.WebSockets +@using System.Text +@inject ModalService modalService +@inject AccountManager AccountManager +@inject AccountState Account + +@**@ + +
+ @if (welcomeModalIsVisible) + { + + } + + + +
+ +@code { + bool welcomeModalIsVisible = false; + string activeSessionName = string.Empty; + ClientWebSocket socket = new ClientWebSocket(); + + protected override async Task OnInitializedAsync() + { + Account.LoginChangedEvent += OnLoginChanged; + var success = await AccountManager.TryLoginSilentAsync(); + if (!success) + { + welcomeModalIsVisible = true; + } + } + + private void OnLoginChanged(object? sender, LoginEventArgs args) + { + welcomeModalIsVisible = args.User == null; + StateHasChanged(); + } + private void OnChangeSession(SessionMetadata s) + { + activeSessionName = s.Name; + StateHasChanged(); + } +} diff --git a/Shogi.UI/Pages/Home/Home.razor.css b/Shogi.UI/Pages/Home/Home.razor.css new file mode 100644 index 0000000..0192b56 --- /dev/null +++ b/Shogi.UI/Pages/Home/Home.razor.css @@ -0,0 +1,30 @@ +.shogi { + display: grid; + grid-template-areas: + "pageHeader board" + "browser board"; + grid-template-columns: minmax(25rem, 25vw) 1fr; + grid-template-rows: max-content 1fr; + place-items: stretch; + gap: 1rem; + padding: 0.5rem; + position: relative; /* For absolute positioned children. */ + background-color: var(--primary-color); +} + + .shogi > ::deep .game-board { + grid-area: board; + place-self: center; + } + + .shogi > ::deep .pageHeader { + grid-area: pageHeader; + } + + .shogi > ::deep .game-browser { + grid-area: browser; + } + + Modals { + display: none; + } \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/LoginModal.razor b/Shogi.UI/Pages/Home/LoginModal.razor new file mode 100644 index 0000000..d1367d5 --- /dev/null +++ b/Shogi.UI/Pages/Home/LoginModal.razor @@ -0,0 +1,56 @@ +@inject AccountManager Account + + +
+
+ @if (guestAccountDescriptionIsVisible) + { +

What's the difference?

+ @**@ +

+ Guest accounts are session based, meaning that the account lives exclusively within the device and browser you create the account on. + This is the only difference between guest and email accounts. +

+
+ Deleting your device's browser storage for this site also deletes your guest account. This data is how you are remembered between sessions. +
+ + } + else + { + +

Welcome to Shogi!

+
+

How would you like to proceed?

+

+ + +

+
+

+ +

+ } +
+
+ +@code { + bool guestAccountDescriptionIsVisible = false; + + void ShowGuestAccountDescription() + { + guestAccountDescriptionIsVisible = true; + } + void HideGuestAccountDescription() + { + guestAccountDescriptionIsVisible = false; + } +} diff --git a/Shogi.UI/Pages/Home/LoginModal.razor.css b/Shogi.UI/Pages/Home/LoginModal.razor.css new file mode 100644 index 0000000..2f3b721 --- /dev/null +++ b/Shogi.UI/Pages/Home/LoginModal.razor.css @@ -0,0 +1,21 @@ +.my-modal-background { + display: grid; + place-items: center; + position: fixed; + background-color: rgba(0,0,0,0.4); + inset: 0; + z-index: 900; +} + +.my-modal { + text-align: center; + background-color: var(--contrast-color); + padding: 1rem; + max-width: 40rem; +} + +.account-description { + display: grid; + grid-template-columns: 1fr max-content max-content; + column-gap: 1.5rem; +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/PageHeader.razor b/Shogi.UI/Pages/Home/PageHeader.razor new file mode 100644 index 0000000..437a26d --- /dev/null +++ b/Shogi.UI/Pages/Home/PageHeader.razor @@ -0,0 +1,31 @@ +@inject AccountState Account +@inject AccountManager AccountManager + + + +@code { + private User? user; + + protected override void OnInitialized() + { + Account.LoginChangedEvent += OnLoginChange; + } + + private void OnLoginChange(object? sender, LoginEventArgs args) + { + if (args == null) + throw new ArgumentException(nameof(args)); + user = args.User; + StateHasChanged(); + } +} diff --git a/Shogi.UI/Pages/Home/PageHeader.razor.css b/Shogi.UI/Pages/Home/PageHeader.razor.css new file mode 100644 index 0000000..9fc25a0 --- /dev/null +++ b/Shogi.UI/Pages/Home/PageHeader.razor.css @@ -0,0 +1,20 @@ +.pageHeader { + display: grid; + grid-template-columns: 1fr max-content; + place-items: center stretch; + place-content: stretch; + padding: 0.5rem; + background-color: var(--contrast-color); +} + + .pageHeader h1 { + place-self: baseline start; + } + + .pageHeader button.logout { + display: block; + width: 100%; + } + .pageHeader .user { + text-align: right; + } \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Pieces/Bishop.razor b/Shogi.UI/Pages/Home/Pieces/Bishop.razor new file mode 100644 index 0000000..8f4d2b9 --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/Bishop.razor @@ -0,0 +1,32 @@ +@if (IsPromoted) +{ + + image/svg+xml龍馬2007-07-2213xforeverjaryuumaryuumeryumaryume龍馬shougishogi将棋board game + + + + + + + + + +} +else +{ + + image/svg+xml角行2007-07-2213xforeverjakakugyoukakugyo角行shougishogi将棋board game + + + + + + + + +} + +@code { + [Parameter] + public bool IsPromoted { get; set; } +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Pieces/ChallengingKing.razor b/Shogi.UI/Pages/Home/Pieces/ChallengingKing.razor new file mode 100644 index 0000000..c7af21b --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/ChallengingKing.razor @@ -0,0 +1,10 @@ + + image/svg+xml玉將2007-07-2213xforeverjagyokushougyokusho玉将shougishogi将棋board game + + + + + + + + \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Pieces/GoldGeneral.razor b/Shogi.UI/Pages/Home/Pieces/GoldGeneral.razor new file mode 100644 index 0000000..2aca8c4 --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/GoldGeneral.razor @@ -0,0 +1,10 @@ + + image/svg+xml金將2007-07-2213xforeverjakinshoukinsho金将shougishogi将棋board game + + + + + + + + \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Pieces/Knight.razor b/Shogi.UI/Pages/Home/Pieces/Knight.razor new file mode 100644 index 0000000..5402a67 --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/Knight.razor @@ -0,0 +1,31 @@ +@if (IsPromoted) +{ + + image/svg+xml成桂2007-07-2213xforeverjanarikei成桂shougishogi将棋board game + + + + + + + + +} +else +{ + + image/svg+xml桂馬2007-07-2213xforeverjakeima桂馬shougishogi将棋board game + + + + + + + + +} + +@code { + [Parameter] + public bool IsPromoted { get; set; } +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Pieces/Lance.razor b/Shogi.UI/Pages/Home/Pieces/Lance.razor new file mode 100644 index 0000000..b1b24cb --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/Lance.razor @@ -0,0 +1,31 @@ +@if (IsPromoted) +{ + + image/svg+xml成香2007-07-2213xforeverjanarikyounarikyo成香shougishogi将棋board game + + + + + + + + +} +else +{ + + image/svg+xml香車2007-07-2213xforeverjakyoushakyosha香車shougishogi将棋board game + + + + + + + + +} + +@code { + [Parameter] + public bool IsPromoted { get; set; } +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Pieces/Pawn.razor b/Shogi.UI/Pages/Home/Pieces/Pawn.razor new file mode 100644 index 0000000..14b9ee4 --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/Pawn.razor @@ -0,0 +1,31 @@ +@if (IsPromoted) +{ + + image/svg+xmlと金2007-07-2213xforeverjatokinと金shougishogi将棋board game + + + + + + + + +} +else +{ + + image/svg+xml歩兵2007-07-2213xforeverjpfuhyoufuhyo歩兵shougishogi将棋board game + + + + + + + + +} + +@code { + [Parameter] + public bool IsPromoted { get; set; } +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Pieces/ReigningKing.razor b/Shogi.UI/Pages/Home/Pieces/ReigningKing.razor new file mode 100644 index 0000000..3fd70bd --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/ReigningKing.razor @@ -0,0 +1,10 @@ + + image/svg+xml王將2007-07-2213xforeverjaoushouosho王将shougishogi将棋board game + + + + + + + + diff --git a/Shogi.UI/Pages/Home/Pieces/Rook.razor b/Shogi.UI/Pages/Home/Pieces/Rook.razor new file mode 100644 index 0000000..e678cbd --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/Rook.razor @@ -0,0 +1,31 @@ +@if (IsPromoted) +{ + + image/svg+xml龍王2007-07-2213xforeverjaryuuouryuo龍王shougishogi将棋board game + + + + + + + + +} +else +{ + + image/svg+xml飛車2007-07-2213xforeverjahisha飛車shougishogi将棋board game + + + + + + + + +} + +@code { + [Parameter] + public bool IsPromoted { get; set; } +} \ No newline at end of file diff --git a/Shogi.UI/Pages/Home/Pieces/SilverGeneral.razor b/Shogi.UI/Pages/Home/Pieces/SilverGeneral.razor new file mode 100644 index 0000000..25fb93a --- /dev/null +++ b/Shogi.UI/Pages/Home/Pieces/SilverGeneral.razor @@ -0,0 +1,31 @@ +@if (IsPromoted) +{ + + image/svg+xml成銀2007-07-2213xforeverjanarigin成銀shougishogi将棋board game + + + + + + + + +} +else +{ + + image/svg+xml銀将2007-07-2213xforeverjaginshouginsho銀将shougishogi将棋board game + + + + + + + + +} + +@code { + [Parameter] + public bool IsPromoted { get; set; } +} \ No newline at end of file diff --git a/Shogi.UI/Program.cs b/Shogi.UI/Program.cs new file mode 100644 index 0000000..ac0d6df --- /dev/null +++ b/Shogi.UI/Program.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Shogi.UI; +using Shogi.UI.Pages.Home.Account; +using Shogi.UI.Pages.Home.Api; +using Shogi.UI.Shared; +using Shogi.UI.Shared.Modal; +using System.Net.WebSockets; +using System.Text.Json; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); +ConfigureDependencies(builder.Services, builder.Configuration); +await builder.Build().RunAsync(); + +static void ConfigureDependencies(IServiceCollection services, IConfiguration configuration) +{ + /** + * Why two HTTP clients? + * See qhttps://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?source=recommendations&view=aspnetcore-6.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client + */ + var shogiApiUrl = new Uri(configuration["ShogiApiUrl"], UriKind.Absolute); + services + .AddHttpClient(ShogiApi.MsalClientName, client => client.BaseAddress = shogiApiUrl) + .AddHttpMessageHandler(); + services + .AddHttpClient(ShogiApi.GuestClientName, client => client.BaseAddress = shogiApiUrl) + .AddHttpMessageHandler(); + services + .AddHttpClient(ShogiApi.AnonymouseClientName, client => client.BaseAddress = shogiApiUrl) + .AddHttpMessageHandler(); + + // Authorization + services.AddMsalAuthentication(options => + { + configuration.Bind("AzureAd", options.ProviderOptions.Authentication); + }); + services.AddOidcAuthentication(options => + { + // Configure your authentication provider options here. + // For more information, see https://aka.ms/blazor-standalone-auth + configuration.Bind("AzureAd", options.ProviderOptions); + options.ProviderOptions.ResponseType = "code"; + }); + + // https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-6.0#service-lifetime + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + var serializerOptions = new JsonSerializerOptions + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + services.AddScoped((sp) => serializerOptions); +} \ No newline at end of file diff --git a/Shogi.UI/Properties/Resources.Designer.cs b/Shogi.UI/Properties/Resources.Designer.cs new file mode 100644 index 0000000..59e260a --- /dev/null +++ b/Shogi.UI/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Shogi.UI.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Shogi.UI.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Shogi.UI/Properties/Resources.resx b/Shogi.UI/Properties/Resources.resx new file mode 100644 index 0000000..4fdb1b6 --- /dev/null +++ b/Shogi.UI/Properties/Resources.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Shogi.UI/Properties/launchSettings.json b/Shogi.UI/Properties/launchSettings.json new file mode 100644 index 0000000..8c7658c --- /dev/null +++ b/Shogi.UI/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "Shogi.UI": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:3000", + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/Shogi.UI/Properties/serviceDependencies.json b/Shogi.UI/Properties/serviceDependencies.json new file mode 100644 index 0000000..44cc45e --- /dev/null +++ b/Shogi.UI/Properties/serviceDependencies.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "identityapp1": { + "type": "identityapp", + "dynamicId": null + } + } +} \ No newline at end of file diff --git a/Shogi.UI/Properties/serviceDependencies.local.json b/Shogi.UI/Properties/serviceDependencies.local.json new file mode 100644 index 0000000..3c85224 --- /dev/null +++ b/Shogi.UI/Properties/serviceDependencies.local.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "identityapp1": { + "type": "identityapp.default", + "dynamicId": null + } + } +} \ No newline at end of file diff --git a/Shogi.UI/Shared/LocalStorage.cs b/Shogi.UI/Shared/LocalStorage.cs new file mode 100644 index 0000000..d71ee8f --- /dev/null +++ b/Shogi.UI/Shared/LocalStorage.cs @@ -0,0 +1,51 @@ +using Microsoft.JSInterop; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Shogi.UI.Shared +{ + public class LocalStorage : ILocalStorage + { + private readonly JsonSerializerOptions jsonOptions; + private readonly IJSRuntime jSRuntime; + + public LocalStorage(IJSRuntime jSRuntime) + { + jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new JsonStringEnumConverter()); + this.jSRuntime = jSRuntime; + } + + public ValueTask Set(string key, T value) + { + var serialized = JsonSerializer.Serialize(value); + return jSRuntime.InvokeVoidAsync("localStorage.setItem", key, serialized); + } + + public async ValueTask Get(string key) where T : struct + { + + var value = await jSRuntime.InvokeAsync("localStorage.getItem", key); + try + { + return JsonSerializer.Deserialize(value, jsonOptions); + } + catch (ArgumentNullException) + { + return default; + } + } + + public ValueTask Delete(string key) + { + return jSRuntime.InvokeVoidAsync("localStorage.removeItem", key); + } + } + + public interface ILocalStorage + { + ValueTask Delete(string key); + ValueTask Get(string key) where T : struct; + ValueTask Set(string key, T value); + } +} diff --git a/Shogi.UI/Shared/LoginDisplay.razor b/Shogi.UI/Shared/LoginDisplay.razor new file mode 100644 index 0000000..1f0d155 --- /dev/null +++ b/Shogi.UI/Shared/LoginDisplay.razor @@ -0,0 +1,24 @@ +@*@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication*@ + +@*@inject NavigationManager Navigation +@inject SignOutSessionStateManager SignOutManager*@ + +@* + + Hello, @context.User.Identity?.Name! + + + + Log in + +*@ + +@code{ + // https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-6.0#customize-the-authentication-user-interface + //private async Task BeginSignOut(MouseEventArgs args) + //{ + // await SignOutManager.SetSignOutState(); + // Navigation.NavigateTo("authentication/logout"); + //} +} diff --git a/Shogi.UI/Shared/MainLayout.razor b/Shogi.UI/Shared/MainLayout.razor new file mode 100644 index 0000000..7ea1402 --- /dev/null +++ b/Shogi.UI/Shared/MainLayout.razor @@ -0,0 +1,4 @@ +@inherits LayoutComponentBase + +@Body + diff --git a/Shogi.UI/Shared/MainLayout.razor.css b/Shogi.UI/Shared/MainLayout.razor.css new file mode 100644 index 0000000..201c67e --- /dev/null +++ b/Shogi.UI/Shared/MainLayout.razor.css @@ -0,0 +1,3 @@ +html, body, #app { + height: 100%; +} diff --git a/Shogi.UI/Shared/Modal/ModalService.cs b/Shogi.UI/Shared/Modal/ModalService.cs new file mode 100644 index 0000000..7068213 --- /dev/null +++ b/Shogi.UI/Shared/Modal/ModalService.cs @@ -0,0 +1,54 @@ +namespace Shogi.UI.Shared.Modal +{ + /// + /// An injectible service which can be invoked to display preset modals via the razor component. + /// Only one modal me be visible at a time. + /// + public class ModalService + { + public event EventHandler? ModalVisibilityChangedEvent; + + public ModalService() + { + } + + public bool LoginModalIsVisible { get; private set; } + public bool GuestAccountDescriptionIsVisible { get; private set; } + + public void ShowLoginModal() + { + SetAllVisibilitiesToFalse(); + LoginModalIsVisible = true; + EmitCurrentState(); + } + + public void ShowGuestAccountDescriptionModal() + { + SetAllVisibilitiesToFalse(); + GuestAccountDescriptionIsVisible = true; + EmitCurrentState(); + } + + public void HideAllModals() + { + SetAllVisibilitiesToFalse(); + EmitCurrentState(); + } + + private void EmitCurrentState() + { + ModalVisibilityChangedEvent?.Invoke(this, new() + { + LoginModalIsVisible = LoginModalIsVisible, + GuestAccountDescriptionIsVisible = GuestAccountDescriptionIsVisible + }); + } + + private void SetAllVisibilitiesToFalse() + { + LoginModalIsVisible = false; + GuestAccountDescriptionIsVisible = false; + } + + } +} diff --git a/Shogi.UI/Shared/Modal/ModalVisibilityChangedEventArgs.cs b/Shogi.UI/Shared/Modal/ModalVisibilityChangedEventArgs.cs new file mode 100644 index 0000000..4ab2064 --- /dev/null +++ b/Shogi.UI/Shared/Modal/ModalVisibilityChangedEventArgs.cs @@ -0,0 +1,9 @@ +namespace Shogi.UI.Shared.Modal +{ + public class ModalVisibilityChangedEventArgs : EventArgs + { + public bool LoginModalIsVisible { get; set; } + public bool GuestAccountDescriptionIsVisible { get; set; } + + } +} diff --git a/Shogi.UI/Shared/Modal/Modals.razor b/Shogi.UI/Shared/Modal/Modals.razor new file mode 100644 index 0000000..5be1583 --- /dev/null +++ b/Shogi.UI/Shared/Modal/Modals.razor @@ -0,0 +1,39 @@ +@inject ModalService modalService +@inject AccountManager Account +@inject NavigationManager NavManager +@inject ILocalStorage localStorage + +@if (shouldShow) +{ +
+
+ @if (modalService.LoginModalIsVisible) + { + + } + else if (modalService.GuestAccountDescriptionIsVisible) + { + + } +
+
+} + +@code { + bool shouldShow = false; + + protected override void OnInitialized() + { + modalService.ModalVisibilityChangedEvent += OnModalChange; + } + + void OnModalChange(object? sender, ModalVisibilityChangedEventArgs args) + { + Console.WriteLine("Modal Change"); + if (args != null) + { + shouldShow = args.LoginModalIsVisible || args.GuestAccountDescriptionIsVisible; + StateHasChanged(); + } + } +} diff --git a/Shogi.UI/Shared/Modal/Modals.razor.css b/Shogi.UI/Shared/Modal/Modals.razor.css new file mode 100644 index 0000000..2f3b721 --- /dev/null +++ b/Shogi.UI/Shared/Modal/Modals.razor.css @@ -0,0 +1,21 @@ +.my-modal-background { + display: grid; + place-items: center; + position: fixed; + background-color: rgba(0,0,0,0.4); + inset: 0; + z-index: 900; +} + +.my-modal { + text-align: center; + background-color: var(--contrast-color); + padding: 1rem; + max-width: 40rem; +} + +.account-description { + display: grid; + grid-template-columns: 1fr max-content max-content; + column-gap: 1.5rem; +} \ No newline at end of file diff --git a/Shogi.UI/Shared/MyNotAuthorized.razor b/Shogi.UI/Shared/MyNotAuthorized.razor new file mode 100644 index 0000000..fe34ce9 --- /dev/null +++ b/Shogi.UI/Shared/MyNotAuthorized.razor @@ -0,0 +1,5 @@ +

MyNotAuthorized

+ +@code { + +} diff --git a/Shogi.UI/Shared/RedirectToLogin.razor b/Shogi.UI/Shared/RedirectToLogin.razor new file mode 100644 index 0000000..d7864b2 --- /dev/null +++ b/Shogi.UI/Shared/RedirectToLogin.razor @@ -0,0 +1,14 @@ +@inject NavigationManager Navigation +@inject ModalService ModalService +@inject AccountManager ShogiService + +@**@ +
Not implemented!
+ +@code { + protected override void OnInitialized() + { + //ModalService.ShowLoginModal(); + //Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}"); + } +} diff --git a/Shogi.UI/Shared/RotatingCogsSvg.razor b/Shogi.UI/Shared/RotatingCogsSvg.razor new file mode 100644 index 0000000..ffbeb21 --- /dev/null +++ b/Shogi.UI/Shared/RotatingCogsSvg.razor @@ -0,0 +1,28 @@ +
+ + + + + + + + + + +
+ +@code { + +} diff --git a/Shogi.UI/Shared/RotatingCogsSvg.razor.css b/Shogi.UI/Shared/RotatingCogsSvg.razor.css new file mode 100644 index 0000000..1effb93 --- /dev/null +++ b/Shogi.UI/Shared/RotatingCogsSvg.razor.css @@ -0,0 +1,15 @@ +.rotating-cogs { + text-align: center; +} + +.left, right { + position: relative; + width: 32px; +} + +.left { + left: 2px; +} +.right { + left: -2px; +} diff --git a/Shogi.UI/Shared/ShogiSocket.cs b/Shogi.UI/Shared/ShogiSocket.cs new file mode 100644 index 0000000..7f64877 --- /dev/null +++ b/Shogi.UI/Shared/ShogiSocket.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Http.Extensions; +using Shogi.Contracts.Socket; +using Shogi.Contracts.Types; +using System.Buffers; +using System.Net.WebSockets; +using System.Text.Json; + +namespace Shogi.UI.Shared; + +public class ShogiSocket : IDisposable +{ + public event EventHandler? OnCreateGameMessage; + + private readonly ClientWebSocket socket; + private readonly JsonSerializerOptions serializerOptions; + private readonly UriBuilder uriBuilder; + private readonly CancellationTokenSource cancelToken; + private readonly IMemoryOwner memoryOwner; + private bool disposedValue; + + public ShogiSocket(IConfiguration configuration, ClientWebSocket socket, JsonSerializerOptions serializerOptions) + { + this.socket = socket; + this.serializerOptions = serializerOptions; + this.uriBuilder = new UriBuilder(configuration["SocketUrl"]); + this.cancelToken = new CancellationTokenSource(); + this.memoryOwner = MemoryPool.Shared.Rent(1024 * 2); + } + + public async Task OpenAsync(string token) + { + uriBuilder.Query = new QueryBuilder + { + { "token", token } + }.ToQueryString().Value; + + await socket.ConnectAsync(this.uriBuilder.Uri, cancelToken.Token); + Console.WriteLine("CONNECTED"); + Listen(); + } + + private async void Listen() + { + while (socket.State == WebSocketState.Open && !cancelToken.IsCancellationRequested) + { + var result = await socket.ReceiveAsync(this.memoryOwner.Memory, cancelToken.Token); + var memory = this.memoryOwner.Memory[..result.Count].ToArray(); + var action = JsonDocument + .Parse(memory[..result.Count]) + .RootElement + .GetProperty(nameof(ISocketResponse.Action)) + .Deserialize(); + + switch (action) + { + case SocketAction.SessionCreated: + this.OnCreateGameMessage?.Invoke(this, JsonSerializer.Deserialize(memory, this.serializerOptions)!); + break; + default: + break; + } + } + if (!cancelToken.IsCancellationRequested) + { + throw new InvalidOperationException("Stopped socket listening without cancelling."); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + cancelToken.Cancel(); + socket.Dispose(); + memoryOwner.Dispose(); + } + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/Shogi.UI/Shogi.UI.csproj b/Shogi.UI/Shogi.UI.csproj new file mode 100644 index 0000000..a62392d --- /dev/null +++ b/Shogi.UI/Shogi.UI.csproj @@ -0,0 +1,48 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/Shogi.UI/_Imports.razor b/Shogi.UI/_Imports.razor new file mode 100644 index 0000000..7d78d6c --- /dev/null +++ b/Shogi.UI/_Imports.razor @@ -0,0 +1,15 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Shogi.UI.Pages.Home.Account +@using Shogi.UI.Pages.Home.Api +@using Shogi.UI.Pages.Home.Pieces +@using Shogi.UI.Shared.Modal +@using Shogi.UI.Shared diff --git a/Shogi.UI/wwwroot/appsettings.Development.json b/Shogi.UI/wwwroot/appsettings.Development.json new file mode 100644 index 0000000..fdae94a --- /dev/null +++ b/Shogi.UI/wwwroot/appsettings.Development.json @@ -0,0 +1,6 @@ +{ + "Local": { + "Authority": "https://login.microsoftonline.com/", + "ClientId": "33333333-3333-3333-33333333333333333" + } +} diff --git a/Shogi.UI/wwwroot/appsettings.json b/Shogi.UI/wwwroot/appsettings.json new file mode 100644 index 0000000..2b1a4de --- /dev/null +++ b/Shogi.UI/wwwroot/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AzureAd": { + "ClientId": "935df672-efa6-45fa-b2e8-b76dfd65a122", + "Authority": "https://login.microsoftonline.com/common", + "ValidateAuthority": true, + "Scopes": [ + "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/DefaultScope" + ] + }, + "ShogiApiUrl2": "https://api.lucaserver.space/Shogi.Sockets/", + "SocketUrl2": "wss://api.lucaserver.space/Shogi.Sockets/", + "ShogiApiUrl": "https://localhost:5001", + "SocketUrl": "wss://localhost:5001" +} \ No newline at end of file diff --git a/Shogi.UI/wwwroot/css/app.css b/Shogi.UI/wwwroot/css/app.css new file mode 100644 index 0000000..341c863 --- /dev/null +++ b/Shogi.UI/wwwroot/css/app.css @@ -0,0 +1,131 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body, #app { + height: 100%; +} + +body { + margin: 0; + padding: 0; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: var(--primary-color); + font-family: cursive; +} + +span { + display: inline-block; + vertical-align: middle; +} + +a { + text-decoration: none; +} + + a.plain { + color: inherit; + } + + a:hover { + text-decoration: underline; + } + +button { + background-color: var(--primary-color); + color: var(--contrast-color); + border: none; + padding: 0.3rem 0.7rem; + border: 1px solid var(--primary-color); + border-radius: 2px; + cursor: pointer; + font: inherit; + font-size: 85%; +} + +.smaller { + font-size: smaller; +} + + + + + + + + +#app { + display: grid; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +p { + margin-bottom: 0.5rem; +} + +/* Layout */ +.flex-between { + display: flex; + justify-content: space-between; +} + +/* Variables */ +:root { + --primary-color: #444; + --secondary-color: #5e5e5e; + --contrast-color: #eaeaea; +} + +/* Bootstrap overrides */ + +h1, h2, h3, h4, h5, h6 { + all: revert; + margin-top: 0; + margin-bottom: 0.5rem; +} + +button.btn-link { + color: #0066cc; +} + +button.btn.btn-link:not(:hover) { + text-decoration: none; +} diff --git a/Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.css b/Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.css new file mode 100644 index 0000000..9465c64 --- /dev/null +++ b/Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.2.0 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors + * Copyright 2011-2022 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid var(--bs-border-color);border-radius:.375rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:var(--bs-table-color);vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:calc(1.5em + .75rem + 2px);padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:.375rem}.form-control-color::-webkit-color-swatch{border-radius:.375rem}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + 2px)}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + 2px)}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.25rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.5rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;width:100%;height:100%;padding:1rem .75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.375rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select,.input-group>:not(:first-child):not(.dropdown-menu):not(.form-floating):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.375rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.375rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check:focus+.btn,.btn:focus{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:active+.btn,.btn-check:checked+.btn,.btn.active,.btn.show,.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:active+.btn:focus,.btn-check:checked+.btn:focus,.btn.active:focus,.btn.show:focus,.btn:active:focus{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:none;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:0.5rem}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:0.25rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:#212529;--bs-dropdown-bg:#fff;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:0.375rem;--bs-dropdown-border-width:1px;--bs-dropdown-inner-border-radius:calc(0.375rem - 1px);--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color:#212529;--bs-dropdown-link-hover-color:#1e2125;--bs-dropdown-link-hover-bg:#e9ecef;--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:1000;display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.375rem}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#6c757d;display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:1px;--bs-nav-tabs-border-color:#dee2e6;--bs-nav-tabs-border-radius:0.375rem;--bs-nav-tabs-link-hover-border-color:#e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color:#495057;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(var(--bs-nav-tabs-border-width) * -1);background:0 0;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(var(--bs-nav-tabs-border-width) * -1);border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:0.375rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(0, 0, 0, 0.55);--bs-navbar-hover-color:rgba(0, 0, 0, 0.7);--bs-navbar-disabled-color:rgba(0, 0, 0, 0.3);--bs-navbar-active-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-hover-color:rgba(0, 0, 0, 0.9);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(0, 0, 0, 0.1);--bs-navbar-toggler-border-radius:0.375rem;--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .show>.nav-link{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-border-width:1px;--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:0.375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(0.375rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(0, 0, 0, 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#fff;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:#000;--bs-accordion-bg:#fff;--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:1px;--bs-accordion-border-radius:0.375rem;--bs-accordion-inner-border-radius:calc(0.375rem - 1px);--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-body-color%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color:#86b7fe;--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:#0c63e4;--bs-accordion-active-bg:#e7f1ff}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(var(--bs-accordion-border-width) * -1) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:#6c757d;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#6c757d;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:#fff;--bs-pagination-border-width:1px;--bs-pagination-border-color:#dee2e6;--bs-pagination-border-radius:0.375rem;--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:#e9ecef;--bs-pagination-hover-border-color:#dee2e6;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:#e9ecef;--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:#6c757d;--bs-pagination-disabled-bg:#fff;--bs-pagination-disabled-border-color:#dee2e6;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:0.5rem}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:0.25rem}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:0.375rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:1px solid var(--bs-alert-border-color);--bs-alert-border-radius:0.375rem;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:#084298;--bs-alert-bg:#cfe2ff;--bs-alert-border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{--bs-alert-color:#41464b;--bs-alert-bg:#e2e3e5;--bs-alert-border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{--bs-alert-color:#0f5132;--bs-alert-bg:#d1e7dd;--bs-alert-border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{--bs-alert-color:#055160;--bs-alert-bg:#cff4fc;--bs-alert-border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{--bs-alert-color:#664d03;--bs-alert-bg:#fff3cd;--bs-alert-border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{--bs-alert-color:#842029;--bs-alert-bg:#f8d7da;--bs-alert-border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{--bs-alert-color:#636464;--bs-alert-bg:#fefefe;--bs-alert-border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{--bs-alert-color:#141619;--bs-alert-bg:#d3d3d4;--bs-alert-border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#e9ecef;--bs-progress-border-radius:0.375rem;--bs-progress-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{--bs-list-group-color:#212529;--bs-list-group-bg:#fff;--bs-list-group-border-color:rgba(0, 0, 0, 0.125);--bs-list-group-border-width:1px;--bs-list-group-border-radius:0.375rem;--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#495057;--bs-list-group-action-hover-color:#495057;--bs-list-group-action-hover-bg:#f8f9fa;--bs-list-group-action-active-color:#212529;--bs-list-group-action-active-bg:#e9ecef;--bs-list-group-disabled-color:#6c757d;--bs-list-group-disabled-bg:#fff;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(var(--bs-list-group-border-width) * -1);border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(var(--bs-list-group-border-width) * -1);border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(255, 255, 255, 0.85);--bs-toast-border-width:1px;--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:0.375rem;--bs-toast-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-toast-header-color:#6c757d;--bs-toast-header-bg:rgba(255, 255, 255, 0.85);--bs-toast-header-border-color:rgba(0, 0, 0, 0.05);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{position:absolute;z-index:1090;width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(var(--bs-toast-padding-x) * -.5);margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:#fff;--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:1px;--bs-modal-border-radius:0.5rem;--bs-modal-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius:calc(0.5rem - 1px);--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:1px;--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(var(--bs-modal-header-padding-y) * -.5) calc(var(--bs-modal-header-padding-x) * -.5) calc(var(--bs-modal-header-padding-y) * -.5) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:#fff;--bs-tooltip-bg:#000;--bs-tooltip-border-radius:0.375rem;--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;padding:var(--bs-tooltip-arrow-height);margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:#fff;--bs-popover-border-width:1px;--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:0.5rem;--bs-popover-inner-border-radius:calc(0.5rem - 1px);--bs-popover-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:var(--bs-heading-color);--bs-popover-header-bg:#f0f0f0;--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:#212529;--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(var(--bs-popover-arrow-width) * -.5);content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;-webkit-animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color: ;--bs-offcanvas-bg:#fff;--bs-offcanvas-border-width:1px;--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:575.98px){.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}}@media (max-width:575.98px){.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:767.98px){.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}}@media (max-width:767.98px){.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:991.98px){.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}}@media (max-width:991.98px){.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1199.98px){.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}}@media (max-width:1199.98px){.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}}@media (max-width:1399.98px){.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(var(--bs-offcanvas-padding-y) * -.5);margin-right:calc(var(--bs-offcanvas-padding-x) * -.5);margin-bottom:calc(var(--bs-offcanvas-padding-y) * -.5)}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(13,110,253,var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(108,117,125,var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(25,135,84,var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(13,202,240,var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(255,193,7,var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(220,53,69,var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(248,249,250,var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(33,37,41,var(--bs-bg-opacity,1))!important}.link-primary{color:#0d6efd!important}.link-primary:focus,.link-primary:hover{color:#0a58ca!important}.link-secondary{color:#6c757d!important}.link-secondary:focus,.link-secondary:hover{color:#565e64!important}.link-success{color:#198754!important}.link-success:focus,.link-success:hover{color:#146c43!important}.link-info{color:#0dcaf0!important}.link-info:focus,.link-info:hover{color:#3dd5f3!important}.link-warning{color:#ffc107!important}.link-warning:focus,.link-warning:hover{color:#ffcd39!important}.link-danger{color:#dc3545!important}.link-danger:focus,.link-danger:hover{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:focus,.link-light:hover{color:#f9fafb!important}.link-dark{color:#212529!important}.link-dark:focus,.link-dark:hover{color:#1a1e21!important}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-1{--bs-border-width:1px}.border-2{--bs-border-width:2px}.border-3{--bs-border-width:3px}.border-4{--bs-border-width:4px}.border-5{--bs-border-width:5px}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-semibold{font-weight:600!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-2xl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.css.map b/Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.css.map new file mode 100644 index 0000000..e57ac2e --- /dev/null +++ b/Shogi.UI/wwwroot/css/bootstrap/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","../../scss/vendor/_rfs.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_color-bg.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["@mixin bsBanner($file, $suffix:\"\") {\n /*!\n * Bootstrap #{$file} v5.2.0 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright 2011-2022 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$prefix}white-rgb: #{to-rgb($white)};\n --#{$prefix}black-rgb: #{to-rgb($black)};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$prefix}gradient: #{$gradient};\n\n // Root and body\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$prefix}root-font-size: #{$font-size-root};\n }\n --#{$prefix}body-font-family: #{$font-family-base};\n @include rfs($font-size-base, --#{$prefix}body-font-size);\n --#{$prefix}body-font-weight: #{$font-weight-base};\n --#{$prefix}body-line-height: #{$line-height-base};\n --#{$prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$prefix}body-text-align: #{$body-text-align};\n }\n --#{$prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n\n // scss-docs-start root-border-var\n --#{$prefix}border-width: #{$border-width};\n --#{$prefix}border-style: #{$border-style};\n --#{$prefix}border-color: #{$border-color};\n --#{$prefix}border-color-translucent: #{$border-color-translucent};\n\n --#{$prefix}border-radius: #{$border-radius};\n --#{$prefix}border-radius-sm: #{$border-radius-sm};\n --#{$prefix}border-radius-lg: #{$border-radius-lg};\n --#{$prefix}border-radius-xl: #{$border-radius-xl};\n --#{$prefix}border-radius-2xl: #{$border-radius-2xl};\n --#{$prefix}border-radius-pill: #{$border-radius-pill};\n // scss-docs-end root-border-var\n\n --#{$prefix}link-color: #{$link-color};\n --#{$prefix}link-hover-color: #{$link-hover-color};\n\n --#{$prefix}code-color: #{$code-color};\n\n --#{$prefix}highlight-bg: #{$mark-bg};\n}\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + ' ' + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n }\n @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + ' ' + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + ' ' + $value;\n }\n\n @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + ' calc(' + $min-width + if($value < 0, ' - ', ' + ') + $variable-width + ')';\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluidVal: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluidVal {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluidVal);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule {\n #{$property}: if($rfs-mode == max-media-query, $fluidVal, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n @include font-size(var(--#{$prefix}root-font-size));\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$prefix}body-font-family);\n @include font-size(var(--#{$prefix}body-font-size));\n font-weight: var(--#{$prefix}body-font-weight);\n line-height: var(--#{$prefix}body-line-height);\n color: var(--#{$prefix}body-color);\n text-align: var(--#{$prefix}body-text-align);\n background-color: var(--#{$prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n opacity: $hr-opacity;\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: var(--#{$prefix}link-color);\n text-decoration: $link-decoration;\n\n &:hover {\n color: var(--#{$prefix}link-hover-color);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`