Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bcd8ed2ac | |||
| 7f3cbcfdef | |||
| 6de8728ce6 | |||
| 7e5951da2d | |||
| 78733295e0 | |||
| 2eba8cdb0e | |||
| c7cf706d6c | |||
| 79dd554afa | |||
| 4c52b3bde4 | |||
| 114025fcfb | |||
| a3f23b199a | |||
| 8c65125b16 | |||
| 334c2fecb5 | |||
| a4b08f4cf1 | |||
| d9f48244aa | |||
| dcbf8a3ac3 | |||
| 357c3d9932 | |||
| 0a415a2292 | |||
| e2a8b771d9 | |||
| 0431fb2950 | |||
| 83dd4cc4a3 | |||
| 1ed1ad09af | |||
| 964f3bfb30 | |||
| fe4b013ed0 | |||
| e87bdf8b52 | |||
| fbdaf29f43 | |||
| b5c6b8244d | |||
| 48ab8f7964 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ obj
|
||||
*.user
|
||||
/Shogi.Database/Shogi.Database.dbmdl
|
||||
/Shogi.Database/Shogi.Database.jfm
|
||||
/Shogi/appsettings.Development.json
|
||||
|
||||
120
BoardRules/BoardRules.cs
Normal file
120
BoardRules/BoardRules.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
//using System.Drawing;
|
||||
//using System.Numerics;
|
||||
|
||||
//namespace BoardRules;
|
||||
|
||||
//public static class PieceMoves
|
||||
//{
|
||||
// public static readonly ICollection<Vector2> GoldGeneralMoves =
|
||||
// [
|
||||
// new(-1, 1),
|
||||
// new(0, 1),
|
||||
// new(1, 1),
|
||||
// new(-1, 0),
|
||||
// new(1, 0),
|
||||
// new(0, -1)
|
||||
// ];
|
||||
|
||||
// public static readonly ICollection<Vector2> PawnMoves = [new(0, 1)];
|
||||
|
||||
// public static readonly ICollection<Vector2> KnightMoves = [new(-1, 2), new(1, 2)];
|
||||
|
||||
// public static readonly ICollection<Vector2> BishopMoves =
|
||||
// [
|
||||
// new(float.NegativeInfinity, float.NegativeInfinity),
|
||||
// new(float.NegativeInfinity, float.PositiveInfinity),
|
||||
// new(float.PositiveInfinity, float.PositiveInfinity),
|
||||
// new(float.PositiveInfinity, float.NegativeInfinity),
|
||||
// ];
|
||||
//}
|
||||
|
||||
//public class BoardRules
|
||||
//{
|
||||
// private readonly Dictionary<string, IPiece> pieces = [];
|
||||
|
||||
// public BoardRules WithSize(int width, int height)
|
||||
// {
|
||||
// this.BoardSize = new Size(width, height);
|
||||
// return this;
|
||||
// }
|
||||
|
||||
// public Size BoardSize { get; private set; }
|
||||
|
||||
// public BoardRules AddPiece(IPiece piece)
|
||||
// {
|
||||
// pieces.Add(piece.Name, piece);
|
||||
// return this;
|
||||
// }
|
||||
//}
|
||||
|
||||
//public class BoardPieceRules(BoardRules rules, IPiece piece)
|
||||
//{
|
||||
// public IPiece Piece { get; } = piece;
|
||||
// public BoardRules WithStartingPositions(ICollection<Vector2> positions)
|
||||
// {
|
||||
// // Validate positions against board size
|
||||
// foreach (var pos in positions)
|
||||
// {
|
||||
// if (pos.X < 0 || pos.Y < 0 || pos.X >= rules.BoardSize.Width || pos.Y >= rules.BoardSize.Height)
|
||||
// {
|
||||
// throw new ArgumentOutOfRangeException(nameof(positions), $"Position {pos} is out of bounds for board size {rules.BoardSize}.");
|
||||
// }
|
||||
// }
|
||||
// // Assuming piece has a way to set starting positions, which it currently does not.
|
||||
// // This is just a placeholder to show intent.
|
||||
// // piece.SetStartingPositions(positions);
|
||||
// return rules;
|
||||
// }
|
||||
//}
|
||||
|
||||
//public interface IPiece
|
||||
//{
|
||||
// public string Name { get; }
|
||||
// public ICollection<Vector2> MoveSet { get; }
|
||||
// public ICollection<Vector2> PromotedMoveSet { get; }
|
||||
|
||||
// /// <summary>
|
||||
// /// The starting positions for this type of piece on the board. There could be one or many.
|
||||
// /// </summary>
|
||||
// public ICollection<Vector2> StartingPositions { get; }
|
||||
//}
|
||||
|
||||
//public class GoldGeneral : IPiece
|
||||
//{
|
||||
// public string Name => nameof(GoldGeneral);
|
||||
// public ICollection<Vector2> MoveSet => PieceMoves.GoldGeneralMoves;
|
||||
// public ICollection<Vector2> PromotedMoveSet => PieceMoves.GoldGeneralMoves;
|
||||
|
||||
// public ICollection<Vector2> StartingPositions => [new(3, 0), new(5, 0), new(4, 1)];
|
||||
//}
|
||||
|
||||
//public class Pawn : IPiece
|
||||
//{
|
||||
// public string Name => nameof(Pawn);
|
||||
// public ICollection<Vector2> MoveSet => PieceMoves.PawnMoves;
|
||||
// public ICollection<Vector2> PromotedMoveSet => PieceMoves.GoldGeneralMoves;
|
||||
//}
|
||||
|
||||
//public class Knight : IPiece
|
||||
//{
|
||||
// public string Name => nameof(Knight);
|
||||
// public ICollection<Vector2> MoveSet => PieceMoves.KnightMoves;
|
||||
// public ICollection<Vector2> PromotedMoveSet => PieceMoves.GoldGeneralMoves;
|
||||
//}
|
||||
|
||||
//public class Bishop : IPiece
|
||||
//{
|
||||
// public string Name => nameof(Bishop);
|
||||
// public ICollection<Vector2> MoveSet => PieceMoves.BishopMoves;
|
||||
// public ICollection<Vector2> PromotedMoveSet => PieceMoves.BishopMoves;
|
||||
//}
|
||||
|
||||
//public class Luke
|
||||
//{
|
||||
// public void Yep()
|
||||
// {
|
||||
// var board = new BoardRules()
|
||||
// .WithSize(9, 9)
|
||||
// .AddPiece(new Pawn())
|
||||
// }
|
||||
//}
|
||||
9
BoardRules/BoardRules.csproj
Normal file
9
BoardRules/BoardRules.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
41
README.md
41
README.md
@@ -3,20 +3,45 @@ A web application for playing the Shogi boardgame with others, developed as a ho
|
||||
The application uses sockets to allow players to enjoy sessions in real time.
|
||||
|
||||
### Technologies used
|
||||
A Blazor UI backed by an Asp.net Core API service which uses Sql Server for presistent storage.
|
||||
|
||||
### Known Issues
|
||||
* The app is intended to support logging in via Microsoft accounts or browser-session (Guest) accounts, but currently Microsoft login does not work.
|
||||
* The workaround is to use the guest login.
|
||||
* On first load of the UI, guest account login will fail.
|
||||
* The workaround is to refresh the page and try again. This issue only happens on first load.
|
||||
A Blazor Web App which uses Sql Server for presistent storage and Identity EF Core for account management.
|
||||
|
||||
|
||||
### Roadmap of features remaining
|
||||
The app is not yet finished, though much of the functionality exists. Here is a list of what remains.
|
||||
|
||||
* Placing pieces from the hand onto the board.
|
||||
* Checkmate experience in UI
|
||||
* Checkmate experience
|
||||
* Preventing the rarely invoked rule where check-mate cannot be gained by placing a pawn from the hand.
|
||||
* Retaining an archive of games played and move history of each game.
|
||||
* Adaptive UI layout for varying viewport (screen) sizes.
|
||||
|
||||
### Database Setup
|
||||
If you don't have them, install the `Data storage and processing` tools through Visual Studio Installer. This gives you a local MSSQL database for you to develop with, which is nice so you aren't touching live data while working.
|
||||
|
||||
After that, you need to set up the database. The database has two sources of table structure.
|
||||
|
||||
#### 1. Shogi.Database project
|
||||
This project contains the table structure for the game.
|
||||
|
||||
1. Build the Shogi.Database project.
|
||||
1. Publish the Shogi.Database project (right click in Solution Explorer).
|
||||
* If you're prompted for a connection string, use the one from `Shogi/appsettings.json`.
|
||||
|
||||
#### 2. EntityFramework via AspNetCore.Identity
|
||||
This solution uses the `Microsoft.AspNetCore.Identity.EntityFrameworkCore` package to offer authentication and authorization. This uses Entity Framework, which comes with tools to setup our database with the necessary table structure for auth.
|
||||
|
||||
1. Install the Entity Framework dotnet tools that come with the project. Via Powershell run this command:
|
||||
```
|
||||
dotnet tool restore
|
||||
```
|
||||
1. Run the database migrations and fill out table structure for auth. Run this command from the MUD.Api project directory:
|
||||
```
|
||||
cd /path/to/solution/Shogi
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
After this, you should be ready to create some test accounts for local development.
|
||||
|
||||
### Creating Test Accounts
|
||||
To create test accounts for local development, make sure to build in DEBUG mode and then use the `/debug/create-test-accounts` endpoint
|
||||
of the API. This will create two accounts. If those accounts already exist, they'll be deleted (along with all game session data associated) and recreated.
|
||||
@@ -1,67 +0,0 @@
|
||||
using Shogi.Contracts.Types;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Api.Extensions;
|
||||
|
||||
public static class ContractsExtensions
|
||||
{
|
||||
public static WhichPlayer ToContract(this Domain.ValueObjects.WhichPlayer player)
|
||||
{
|
||||
return player switch
|
||||
{
|
||||
Domain.ValueObjects.WhichPlayer.Player1 => WhichPlayer.Player1,
|
||||
Domain.ValueObjects.WhichPlayer.Player2 => WhichPlayer.Player2,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static WhichPiece ToContract(this Domain.ValueObjects.WhichPiece piece)
|
||||
{
|
||||
return piece switch
|
||||
{
|
||||
Domain.ValueObjects.WhichPiece.King => WhichPiece.King,
|
||||
Domain.ValueObjects.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral,
|
||||
Domain.ValueObjects.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral,
|
||||
Domain.ValueObjects.WhichPiece.Bishop => WhichPiece.Bishop,
|
||||
Domain.ValueObjects.WhichPiece.Rook => WhichPiece.Rook,
|
||||
Domain.ValueObjects.WhichPiece.Knight => WhichPiece.Knight,
|
||||
Domain.ValueObjects.WhichPiece.Lance => WhichPiece.Lance,
|
||||
Domain.ValueObjects.WhichPiece.Pawn => WhichPiece.Pawn,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static Piece ToContract(this Domain.ValueObjects.Piece piece) => new()
|
||||
{
|
||||
IsPromoted = piece.IsPromoted,
|
||||
Owner = piece.Owner.ToContract(),
|
||||
WhichPiece = piece.WhichPiece.ToContract()
|
||||
};
|
||||
|
||||
public static IReadOnlyList<Piece> ToContract(this List<Domain.ValueObjects.Piece> pieces)
|
||||
{
|
||||
return pieces
|
||||
.Select(ToContract)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
public static Dictionary<string, Piece?> ToContract(this ReadOnlyDictionary<string, Domain.ValueObjects.Piece?> boardState) =>
|
||||
boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract());
|
||||
|
||||
public static Domain.ValueObjects.WhichPiece ToDomain(this WhichPiece piece)
|
||||
{
|
||||
return piece switch
|
||||
{
|
||||
WhichPiece.King => Domain.ValueObjects.WhichPiece.King,
|
||||
WhichPiece.GoldGeneral => Domain.ValueObjects.WhichPiece.GoldGeneral,
|
||||
WhichPiece.SilverGeneral => Domain.ValueObjects.WhichPiece.SilverGeneral,
|
||||
WhichPiece.Bishop => Domain.ValueObjects.WhichPiece.Bishop,
|
||||
WhichPiece.Rook => Domain.ValueObjects.WhichPiece.Rook,
|
||||
WhichPiece.Knight => Domain.ValueObjects.WhichPiece.Knight,
|
||||
WhichPiece.Lance => Domain.ValueObjects.WhichPiece.Lance,
|
||||
WhichPiece.Pawn => Domain.ValueObjects.WhichPiece.Pawn,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shogi.Api;
|
||||
using Shogi.Api.Application;
|
||||
using Shogi.Api.Controllers;
|
||||
using Shogi.Api.Identity;
|
||||
using Shogi.Api.Repositories;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var allowedOrigins = builder
|
||||
.Configuration
|
||||
.GetSection("Cors:AllowedOrigins")
|
||||
.Get<string[]>() ?? throw new InvalidOperationException("Configuration for allowed origins is missing.");
|
||||
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.WriteIndented = true;
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddTransient<SessionRepository>();
|
||||
builder.Services.AddTransient<QueryRepository>();
|
||||
builder.Services.AddTransient<ShogiApplication>();
|
||||
builder.Services.AddTransient<GameHubContext>();
|
||||
builder.Services.AddHttpClient<IEmailSender, EmailSender>();
|
||||
builder.Services.Configure<ApiKeys>(builder.Configuration.GetSection("ApiKeys"));
|
||||
|
||||
AddIdentity(builder, builder.Configuration);
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddResponseCompression(opts =>
|
||||
{
|
||||
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]);
|
||||
});
|
||||
var app = builder.Build();
|
||||
|
||||
app.MyMapIdentityApi<ShogiUser>(builder.Environment);
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseHttpsRedirection(); // Apache handles HTTPS in production.
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
}
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options => options.DocumentTitle = "Shogi.Api");
|
||||
app.UseAuthorization();
|
||||
app.Map("/", () => "OK");
|
||||
app.MapControllers();
|
||||
app.UseCors(policy =>
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins).AllowAnyHeader().AllowAnyMethod().AllowCredentials();
|
||||
});
|
||||
|
||||
app.MapHub<GameHub>("/gamehub");
|
||||
|
||||
app.Run();
|
||||
|
||||
static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager configuration)
|
||||
{
|
||||
builder.Services
|
||||
.AddAuthorizationBuilder()
|
||||
.AddPolicy("Admin", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireAssertion(context => context.User?.Identity?.Name switch
|
||||
{
|
||||
"Hauth@live.com" => true,
|
||||
"aat-account" => true,
|
||||
_ => false
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddDbContext<ApplicationDbContext>(options =>
|
||||
{
|
||||
var cs = configuration.GetConnectionString("ShogiDatabase") ?? throw new InvalidOperationException("Database not configured.");
|
||||
options.UseSqlServer(cs);
|
||||
|
||||
// This is helpful to debug account issues without affecting the database.
|
||||
//options.UseInMemoryDatabase("AppDb");
|
||||
})
|
||||
.AddIdentityApiEndpoints<ShogiUser>(options =>
|
||||
{
|
||||
options.SignIn.RequireConfirmedEmail = true;
|
||||
options.User.RequireUniqueEmail = true;
|
||||
})
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||
|
||||
builder.Services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.SlidingExpiration = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(3);
|
||||
});
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Kestrel": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/",
|
||||
"AZURE_USERNAME": "Hauth@live.com"
|
||||
},
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:50728/",
|
||||
"sslPort": 44315
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>5</AnalysisLevel>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>False</GenerateDocumentationFile>
|
||||
<SignAssembly>False</SignAssembly>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>973a1f5f-ef25-4f1c-a24d-b0fc7d016ab8</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Repositories\CouchModels\**" />
|
||||
<Content Remove="Repositories\CouchModels\**" />
|
||||
<EmbeddedResource Remove="Repositories\CouchModels\**" />
|
||||
<None Remove="Repositories\CouchModels\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Repositories\GameboardRepository.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="FluentValidation" Version="11.10.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Shogi.Contracts\Shogi.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Shogi.Domain\Shogi.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"ApiKeys": {
|
||||
"BrevoEmailService": "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"
|
||||
},
|
||||
"TestUserPassword": "I'mAToysRUsK1d"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<AnalysisLevel>5</AnalysisLevel>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||
<Title>Shogi Service Models</Title>
|
||||
<Description>Contains DTOs use for http requests to Shogi backend services.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Api\Queries\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Shogi.Contracts;
|
||||
|
||||
public class ShogiApiJsonSerializerSettings
|
||||
{
|
||||
public readonly static JsonSerializerOptions SystemTextJsonSerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Shogi.Contracts.Types
|
||||
{
|
||||
public class Piece
|
||||
{
|
||||
public bool IsPromoted { get; set; }
|
||||
public WhichPiece WhichPiece { get; set; }
|
||||
public WhichPlayer Owner { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
-- Create a user named Shogi.Api
|
||||
-- Create a user named Shogi
|
||||
|
||||
-- Create a role and grant execute permission to that role
|
||||
--CREATE ROLE db_executor
|
||||
--GRANT EXECUTE To db_executor
|
||||
|
||||
-- Give Shogi.Api user permission to db_executor, db_datareader, db_datawriter
|
||||
-- Give Shogi user permission to db_executor, db_datareader, db_datawriter
|
||||
|
||||
|
||||
/**
|
||||
@@ -13,5 +13,5 @@
|
||||
*
|
||||
* 2. Setup Entity Framework because that's what the login system uses.
|
||||
* 2.a. Install the Entity Framework dotnet tools, via power shell run this command: dotnet tool install --global dotnet-ef
|
||||
* 2.b. To setup the Entity Framework users database, run this powershell command using Shogi.Api as the target project: dotnet ef database update
|
||||
* 2.b. To setup the Entity Framework users database, run this powershell command using Shogi as the target project: dotnet ef database update
|
||||
*/
|
||||
@@ -1,20 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System" />
|
||||
<Using Include="System.Collections.Generic" />
|
||||
<Using Include="System.Linq" />
|
||||
<Using Include="System.Numerics" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Entities\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,47 +0,0 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
internal record class Bishop : Piece
|
||||
{
|
||||
private static readonly ReadOnlyCollection<Path> BishopPaths = new(new List<Path>(4)
|
||||
{
|
||||
new Path(Direction.ForwardLeft, Distance.MultiStep),
|
||||
new Path(Direction.ForwardRight, Distance.MultiStep),
|
||||
new Path(Direction.BackwardLeft, Distance.MultiStep),
|
||||
new Path(Direction.BackwardRight, Distance.MultiStep)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> PromotedBishopPaths = new(new List<Path>(8)
|
||||
{
|
||||
new Path(Direction.Forward),
|
||||
new Path(Direction.Left),
|
||||
new Path(Direction.Right),
|
||||
new Path(Direction.Backward),
|
||||
new Path(Direction.ForwardLeft, Distance.MultiStep),
|
||||
new Path(Direction.ForwardRight, Distance.MultiStep),
|
||||
new Path(Direction.BackwardLeft, Distance.MultiStep),
|
||||
new Path(Direction.BackwardRight, Distance.MultiStep)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
BishopPaths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2PromotedPaths =
|
||||
PromotedBishopPaths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Bishop(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Bishop, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<Path> MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
internal record class Knight : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(2)
|
||||
{
|
||||
new Path(Direction.KnightLeft),
|
||||
new Path(Direction.KnightRight)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Knight(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Knight, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
internal record class Lance : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
||||
{
|
||||
new Path(Direction.Forward, Distance.MultiStep),
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Lance(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Lance, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
public record MoveResult(bool IsSuccess, string Reason = "")
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
internal record class Pawn : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
||||
{
|
||||
new Path(Direction.Forward)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Pawn(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Pawn, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
[DebuggerDisplay("{WhichPiece} {Owner}")]
|
||||
public abstract record class Piece
|
||||
{
|
||||
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<Path> MoveSet { get; }
|
||||
public WhichPiece WhichPiece { get; }
|
||||
public WhichPlayer Owner { get; private set; }
|
||||
public bool IsPromoted { get; private set; }
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Prep the piece for capture by changing ownership and demoting.
|
||||
/// </summary>
|
||||
public void Capture(WhichPlayer newOwner)
|
||||
{
|
||||
Owner = newOwner;
|
||||
IsPromoted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Respecting the move-set of the Piece, collect all positions along the shortest path from start to end.
|
||||
/// Useful if you need to iterate a move-set.
|
||||
/// </summary>
|
||||
/// <param name="start"></param>
|
||||
/// <param name="end"></param>
|
||||
/// <returns>An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions.</returns>
|
||||
public IEnumerable<Vector2> GetPathFromStartToEnd(Vector2 start, Vector2 end)
|
||||
{
|
||||
var steps = new List<Vector2>(10);
|
||||
|
||||
var path = GetNearestPath(MoveSet, start, end);
|
||||
var position = start;
|
||||
while (Vector2.Distance(start, position) < Vector2.Distance(start, end))
|
||||
{
|
||||
position += path.Step;
|
||||
steps.Add(position);
|
||||
|
||||
if (path.Distance == Distance.OneStep) break;
|
||||
}
|
||||
|
||||
if (position == end)
|
||||
{
|
||||
return steps;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static Path GetNearestPath(IEnumerable<Path> paths, Vector2 start, Vector2 end)
|
||||
{
|
||||
if (!paths.DefaultIfEmpty().Any())
|
||||
{
|
||||
throw new ArgumentException("No paths to get nearest path from.");
|
||||
}
|
||||
|
||||
var shortestPath = paths.First();
|
||||
foreach (var path in paths.Skip(1))
|
||||
{
|
||||
var distance = Vector2.Distance(start + path.Step, end);
|
||||
var shortestDistance = Vector2.Distance(start + shortestPath.Step, end);
|
||||
if (distance < shortestDistance)
|
||||
{
|
||||
shortestPath = path;
|
||||
}
|
||||
}
|
||||
return shortestPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
internal record class SilverGeneral : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(4)
|
||||
{
|
||||
new Path(Direction.Forward),
|
||||
new Path(Direction.ForwardLeft),
|
||||
new Path(Direction.ForwardRight),
|
||||
new Path(Direction.BackwardLeft),
|
||||
new Path(Direction.BackwardRight)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public SilverGeneral(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.SilverGeneral, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD;
|
||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
internal class StandardRules
|
||||
{
|
||||
private readonly BoardState boardState;
|
||||
|
||||
internal StandardRules(BoardState board)
|
||||
{
|
||||
boardState = board;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the last move put the player who moved in check.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This strategy recognizes that a "discover check" could only occur from a subset of pieces: Rook, Bishop, Lance.
|
||||
/// In this way, only those pieces need to be considered when evaluating if a move placed the moving player in check.
|
||||
/// </remarks>
|
||||
internal bool DidPlayerPutThemselfInCheck()
|
||||
{
|
||||
if (boardState.PreviousMove.From == null)
|
||||
{
|
||||
// You can't place yourself in check by placing a piece from your hand.
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousMovedPiece = boardState[boardState.PreviousMove.To];
|
||||
if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMove.To}.");
|
||||
var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition;
|
||||
|
||||
|
||||
var isDiscoverCheck = false;
|
||||
// Get line equation from king through the now-unoccupied location.
|
||||
var direction = Vector2.Subtract(kingPosition, boardState.PreviousMove.From.Value);
|
||||
var slope = Math.Abs(direction.Y / direction.X);
|
||||
var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From.Value, Vector2.Normalize(direction));
|
||||
var threat = boardState.QueryFirstPieceInPath(path);
|
||||
if (threat == null || threat.Owner == previousMovedPiece.Owner) return false;
|
||||
// If absolute slope is 45°, look for a bishop along the line.
|
||||
// If absolute slope is 0° or 90°, look for a rook along the line.
|
||||
// if absolute slope is 0°, look for lance along the line.
|
||||
if (float.IsInfinity(slope))
|
||||
{
|
||||
isDiscoverCheck = threat.WhichPiece switch
|
||||
{
|
||||
WhichPiece.Lance => !threat.IsPromoted,
|
||||
WhichPiece.Rook => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
else if (slope == 1)
|
||||
{
|
||||
isDiscoverCheck = threat.WhichPiece switch
|
||||
{
|
||||
WhichPiece.Bishop => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
else if (slope == 0)
|
||||
{
|
||||
isDiscoverCheck = threat.WhichPiece switch
|
||||
{
|
||||
WhichPiece.Rook => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
return isDiscoverCheck;
|
||||
}
|
||||
|
||||
internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To);
|
||||
|
||||
internal bool IsOpposingKingThreatenedByPosition(Vector2 position)
|
||||
{
|
||||
var previousMovedPiece = boardState[position];
|
||||
if (previousMovedPiece == null) return false;
|
||||
|
||||
var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player2KingPosition : boardState.Player1KingPosition;
|
||||
var path = previousMovedPiece.GetPathFromStartToEnd(position, kingPosition);
|
||||
var threatenedPiece = boardState.QueryFirstPieceInPath(path);
|
||||
if (!path.Any() || threatenedPiece == null) return false;
|
||||
|
||||
return threatenedPiece.WhichPiece == WhichPiece.King;
|
||||
}
|
||||
|
||||
internal bool IsOpponentInCheckMate()
|
||||
{
|
||||
// Assume checkmate, then try to disprove.
|
||||
if (!boardState.InCheck.HasValue) return false;
|
||||
// Get all pieces from opponent who threaten the king in question.
|
||||
var opponent = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||
var tilesOccupiedByOpponent = boardState.GetTilesOccupiedBy(opponent);
|
||||
var kingPosition = boardState.WhoseTurn == WhichPlayer.Player1
|
||||
? boardState.Player1KingPosition
|
||||
: boardState.Player2KingPosition;
|
||||
var threats = tilesOccupiedByOpponent.Where(tile => PieceHasLineOfSight(tile, kingPosition)).ToList();
|
||||
if (threats.Count == 1)
|
||||
{
|
||||
/* If there is exactly one threat it is possible to block the check.
|
||||
* Foreach piece owned by whichPlayer
|
||||
* if piece can intercept check, return false;
|
||||
*/
|
||||
var threat = threats.Single();
|
||||
var pathFromThreatToKing = threat.Value.GetPathFromStartToEnd(threat.Key, kingPosition);
|
||||
var tilesThatCouldBlockTheThreat = boardState.GetTilesOccupiedBy(boardState.WhoseTurn);
|
||||
foreach (var threatBlockingPosition in pathFromThreatToKing)
|
||||
{
|
||||
var tilesThatDoBlockThreat = tilesThatCouldBlockTheThreat
|
||||
.Where(tile => PieceHasLineOfSight(tile, threatBlockingPosition))
|
||||
.ToList();
|
||||
|
||||
if (tilesThatDoBlockThreat.Any())
|
||||
{
|
||||
return false; // Cannot be check-mate if a piece can intercept the threat.
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* If no ability to block the check, maybe the king can evade check by moving.
|
||||
*/
|
||||
|
||||
foreach (var maybeSafePosition in GetPossiblePositionsForKing(boardState.WhoseTurn))
|
||||
{
|
||||
threats = tilesOccupiedByOpponent
|
||||
.Where(tile => PieceHasLineOfSight(tile, maybeSafePosition))
|
||||
.ToList();
|
||||
if (!threats.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Vector2> GetPossiblePositionsForKing(WhichPlayer whichPlayer)
|
||||
{
|
||||
var kingPosition = whichPlayer == WhichPlayer.Player1
|
||||
? boardState.Player1KingPosition
|
||||
: boardState.Player2KingPosition;
|
||||
|
||||
var paths = boardState[kingPosition]!.MoveSet;
|
||||
return paths
|
||||
.Select(path => path.Step + kingPosition)
|
||||
// Because the king could be on the edge of the board, where some of its paths do not make sense.
|
||||
.Where(newPosition => newPosition.IsInsideBoardBoundary())
|
||||
// Where tile at position is empty, meaning the king could move there.
|
||||
.Where(newPosition => boardState[newPosition] == null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private bool PieceHasLineOfSight(BoardTile tile, Vector2 lineOfSightTarget)
|
||||
{
|
||||
var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget);
|
||||
return path
|
||||
.SkipLast(1)
|
||||
.All(position => boardState[Notation.ToBoardNotation(position)] == null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Shogi.Domain.ValueObjects
|
||||
{
|
||||
public enum WhichPlayer
|
||||
{
|
||||
Player1,
|
||||
Player2
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using Shogi.Domain.ValueObjects;
|
||||
|
||||
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD
|
||||
{
|
||||
internal static class DomainExtensions
|
||||
{
|
||||
public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King;
|
||||
|
||||
public static bool IsBetween(this float self, float min, float max)
|
||||
{
|
||||
return self >= min && self <= max;
|
||||
}
|
||||
|
||||
public static bool IsInsideBoardBoundary(this Vector2 self)
|
||||
{
|
||||
return self.X.IsBetween(0, 8) && self.Y.IsBetween(0, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD
|
||||
{
|
||||
public static class Notation
|
||||
{
|
||||
private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
|
||||
private static readonly char A = 'A';
|
||||
|
||||
public static string ToBoardNotation(Vector2 vector)
|
||||
{
|
||||
return ToBoardNotation((int)vector.X, (int)vector.Y);
|
||||
}
|
||||
|
||||
public static string ToBoardNotation(int x, int y)
|
||||
{
|
||||
var file = (char)(x + A);
|
||||
var rank = y + 1;
|
||||
return $"{file}{rank}";
|
||||
}
|
||||
public static Vector2 FromBoardNotation(string notation)
|
||||
{
|
||||
if (Regex.IsMatch(notation, BoardNotationRegex))
|
||||
{
|
||||
var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase);
|
||||
char file = match.Groups["file"].Value[0];
|
||||
int rank = int.Parse(match.Groups["rank"].Value);
|
||||
return new Vector2(file - A, rank - 1);
|
||||
}
|
||||
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
|
||||
public static class Direction
|
||||
{
|
||||
public static readonly Vector2 Forward = new(0, 1);
|
||||
public static readonly Vector2 Backward = new(0, -1);
|
||||
public static readonly Vector2 Left = new(-1, 0);
|
||||
public static readonly Vector2 Right = new(1, 0);
|
||||
public static readonly Vector2 ForwardLeft = new(-1, 1);
|
||||
public static readonly Vector2 ForwardRight = new(1, 1);
|
||||
public static readonly Vector2 BackwardLeft = new(-1, -1);
|
||||
public static readonly Vector2 BackwardRight = new(1, -1);
|
||||
public static readonly Vector2 KnightLeft = new(-1, 2);
|
||||
public static readonly Vector2 KnightRight = new(1, 2);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# Shogi.Domain
|
||||
|
||||
### TODO:
|
||||
* There are enough non-DDD classes around navigating the standard 9x9 Shogi board that probably a new value object or entity is merited. See classes within Pathing folder as well as Notation.cs.
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"microsoft.dotnet-msidentity": {
|
||||
"version": "1.0.5",
|
||||
"commands": [
|
||||
"dotnet-msidentity"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
namespace Shogi.UI.Identity;
|
||||
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Handles state for cookie-based auth.
|
||||
/// </summary>
|
||||
public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IAccountManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Map the JavaScript-formatted properties to C#-formatted classes.
|
||||
/// </summary>
|
||||
private readonly JsonSerializerOptions jsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Special auth client.
|
||||
/// </summary>
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Authentication state.
|
||||
/// </summary>
|
||||
private bool _authenticated = false;
|
||||
|
||||
/// <summary>
|
||||
/// Default principal for anonymous (not authenticated) users.
|
||||
/// </summary>
|
||||
private readonly ClaimsPrincipal Unauthenticated =
|
||||
new(new ClaimsIdentity());
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of the auth provider.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">Factory to retrieve auth client.</param>
|
||||
public CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory)
|
||||
=> _httpClient = httpClientFactory.CreateClient("Auth");
|
||||
|
||||
/// <summary>
|
||||
/// Register a new user.
|
||||
/// </summary>
|
||||
/// <param name="email">The user's email address.</param>
|
||||
/// <param name="password">The user's password.</param>
|
||||
/// <returns>The result serialized to a <see cref="FormResult"/>.
|
||||
/// </returns>
|
||||
public async Task<FormResult> RegisterAsync(string email, string password)
|
||||
{
|
||||
string[] defaultDetail = ["An unknown error prevented registration from succeeding."];
|
||||
|
||||
try
|
||||
{
|
||||
// make the request
|
||||
var result = await _httpClient.PostAsJsonAsync("register", new
|
||||
{
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
// successful?
|
||||
if (result.IsSuccessStatusCode)
|
||||
{
|
||||
return new FormResult { Succeeded = true };
|
||||
}
|
||||
|
||||
// body should contain details about why it failed
|
||||
var details = await result.Content.ReadAsStringAsync();
|
||||
var problemDetails = JsonDocument.Parse(details);
|
||||
var errors = new List<string>();
|
||||
var errorList = problemDetails.RootElement.GetProperty("errors");
|
||||
|
||||
foreach (var errorEntry in errorList.EnumerateObject())
|
||||
{
|
||||
if (errorEntry.Value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
errors.Add(errorEntry.Value.GetString()!);
|
||||
}
|
||||
else if (errorEntry.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
errors.AddRange(
|
||||
errorEntry.Value.EnumerateArray().Select(
|
||||
e => e.GetString() ?? string.Empty)
|
||||
.Where(e => !string.IsNullOrEmpty(e)));
|
||||
}
|
||||
}
|
||||
|
||||
// return the error list
|
||||
return new FormResult
|
||||
{
|
||||
Succeeded = false,
|
||||
ErrorList = problemDetails == null ? defaultDetail : [.. errors]
|
||||
};
|
||||
}
|
||||
catch { }
|
||||
|
||||
// unknown error
|
||||
return new FormResult
|
||||
{
|
||||
Succeeded = false,
|
||||
ErrorList = defaultDetail
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User login.
|
||||
/// </summary>
|
||||
/// <param name="email">The user's email address.</param>
|
||||
/// <param name="password">The user's password.</param>
|
||||
/// <returns>The result of the login request serialized to a <see cref="FormResult"/>.</returns>
|
||||
public async Task<FormResult> LoginAsync(string email, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
// login with cookies
|
||||
var result = await _httpClient.PostAsJsonAsync("login?useCookies=true", new
|
||||
{
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
// success?
|
||||
if (result.IsSuccessStatusCode)
|
||||
{
|
||||
// need to refresh auth state
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
|
||||
// success!
|
||||
return new FormResult { Succeeded = true };
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// unknown error
|
||||
return new FormResult
|
||||
{
|
||||
Succeeded = false,
|
||||
ErrorList = ["Invalid email and/or password."]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get authentication state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Called by Blazor anytime and authentication-based decision needs to be made, then cached
|
||||
/// until the changed state notification is raised.
|
||||
/// </remarks>
|
||||
/// <returns>The authentication state asynchronous request.</returns>
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
_authenticated = false;
|
||||
|
||||
// default to not authenticated
|
||||
var user = Unauthenticated;
|
||||
|
||||
try
|
||||
{
|
||||
// the user info endpoint is secured, so if the user isn't logged in this will fail
|
||||
var userResponse = await _httpClient.GetAsync("manage/info");
|
||||
|
||||
// throw if user info wasn't retrieved
|
||||
userResponse.EnsureSuccessStatusCode();
|
||||
|
||||
// user is authenticated,so let's build their authenticated identity
|
||||
var userJson = await userResponse.Content.ReadAsStringAsync();
|
||||
var userInfo = JsonSerializer.Deserialize<UserInfo>(userJson, jsonSerializerOptions);
|
||||
|
||||
if (userInfo != null)
|
||||
{
|
||||
// in our system name and email are the same
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, userInfo.Email),
|
||||
new(ClaimTypes.Email, userInfo.Email)
|
||||
};
|
||||
|
||||
// add any additional claims
|
||||
claims.AddRange(
|
||||
userInfo.Claims
|
||||
.Where(c => c.Key != ClaimTypes.Name && c.Key != ClaimTypes.Email)
|
||||
.Select(c => new Claim(c.Key, c.Value)));
|
||||
|
||||
// tap the roles endpoint for the user's roles
|
||||
var rolesResponse = await _httpClient.GetAsync("roles");
|
||||
|
||||
// throw if request fails
|
||||
rolesResponse.EnsureSuccessStatusCode();
|
||||
|
||||
// read the response into a string
|
||||
var rolesJson = await rolesResponse.Content.ReadAsStringAsync();
|
||||
|
||||
// deserialize the roles string into an array
|
||||
var roles = JsonSerializer.Deserialize<RoleClaim[]>(rolesJson, jsonSerializerOptions);
|
||||
|
||||
// if there are roles, add them to the claims collection
|
||||
if (roles?.Length > 0)
|
||||
{
|
||||
foreach (var role in roles)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(role.Type) && !string.IsNullOrEmpty(role.Value))
|
||||
{
|
||||
claims.Add(new Claim(role.Type, role.Value, role.ValueType, role.Issuer, role.OriginalIssuer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set the principal
|
||||
var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider));
|
||||
user = new ClaimsPrincipal(id);
|
||||
_authenticated = true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// return the state
|
||||
return new AuthenticationState(user);
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
const string Empty = "{}";
|
||||
var emptyContent = new StringContent(Empty, Encoding.UTF8, "application/json");
|
||||
await _httpClient.PostAsync("logout", emptyContent);
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
}
|
||||
|
||||
public async Task<bool> CheckAuthenticatedAsync()
|
||||
{
|
||||
await GetAuthenticationStateAsync();
|
||||
return _authenticated;
|
||||
}
|
||||
|
||||
public class RoleClaim
|
||||
{
|
||||
public string? Issuer { get; set; }
|
||||
public string? OriginalIssuer { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string? Value { get; set; }
|
||||
public string? ValueType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Http;
|
||||
|
||||
namespace Shogi.UI.Identity;
|
||||
|
||||
public class CookieCredentialsMessageHandler : DelegatingHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
|
||||
request.Headers.Add("X-Requested-With", ["XMLHttpRequest"]);
|
||||
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
.MainLayout {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: 100vh;
|
||||
place-items: stretch;
|
||||
}
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
.MainLayout {
|
||||
grid-template-columns: min-content max-content;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
@inject NavigationManager navigator
|
||||
@inject ShogiApi Api
|
||||
|
||||
<div class="NavMenu PrimaryTheme ThemeVariant--Contrast">
|
||||
<h1>Shogi</h1>
|
||||
<p>
|
||||
<a href="">Home</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="search">Search</a>
|
||||
</p>
|
||||
|
||||
<AuthorizeView>
|
||||
<p>
|
||||
<button class="href" @onclick="CreateSession">Create</button>
|
||||
</p>
|
||||
</AuthorizeView>
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<p>@context.User.Identity?.Name</p>
|
||||
<p>
|
||||
<a href="logout">Logout</a>
|
||||
</p>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<p>
|
||||
<a href="login">Login</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="register">Register</a>
|
||||
</p>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
async Task CreateSession()
|
||||
{
|
||||
var sessionId = await Api.PostSession();
|
||||
if (!string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
navigator.NavigateTo($"play/{sessionId}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
.NavMenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 2px solid #444;
|
||||
}
|
||||
|
||||
.NavMenu > * {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
.NavMenu h1 {
|
||||
}
|
||||
|
||||
.NavMenu .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
@page "/"
|
||||
|
||||
@using Shogi.Contracts.Types
|
||||
@using System.Net.WebSockets
|
||||
@using System.Text
|
||||
|
||||
<main class="shogi PrimaryTheme">
|
||||
<h2>What is Shogi?</h2>
|
||||
<p>Shogi is a two-player strategy game where each player simultaneously protects their king while capturing their opponent's.</p>
|
||||
<p>Players take turns, moving one piece each turn until check-mate is achieved.</p>
|
||||
|
||||
<h2>How to Play</h2>
|
||||
|
||||
<h3>Setup</h3>
|
||||
<p>Arrange the board so it looks like this. Take note of the Rook and Bishop positions for each player.</p>
|
||||
<BoardSetupVisualAid />
|
||||
|
||||
<!-- Margin top is because chromium browsers do not render nested grids the same as Firefox -->
|
||||
<h3 style="margin-top: 2rem">Pieces and Movement</h3>
|
||||
<p>Each piece has a unique set of moves. Some pieces, like the Pawn, may move only one tile per turn. Other pieces, like the Bishop, may move multiple tiles per turn.</p>
|
||||
<p>A tile may only hold one piece and, except for the Knight, pieces may never move through each other.</p>
|
||||
<p>Should your piece enter the tile of an opponent's piece, you must stop there and capture the opponent's piece.</p>
|
||||
<PieceMovesVisualAid />
|
||||
|
||||
<h3>Promotion</h3>
|
||||
<p>The furthest three ranks from your starting position is an area called the <b>promotion zone</b>. A piece may promote at the end of the turn when it moves in to, out of, or within the promotion zone.</p>
|
||||
<p>Promoting changes the move-set available to the peice, and a piece <em>must promote</em> if it has no legal, future moves. An example of this is a Pawn moving the the furthest rank on the board such that it cannot go further. In this case, the Pawn must promote.</p>
|
||||
<p>All pieces may promote <b>except for</b> the Gold General and King.</p>
|
||||
<PromotedPieceVisualAid />
|
||||
|
||||
<h3>Capturing and the Hand</h3>
|
||||
<h3>The King and "Check"</h3>
|
||||
<h3>Victory</h3>
|
||||
</main>
|
||||
|
||||
@code {
|
||||
private bool show = true;
|
||||
private string activeSessionName = string.Empty;
|
||||
private Task OnLoginChanged()
|
||||
{
|
||||
StateHasChanged();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
private void OnChangeSession(SessionMetadata s)
|
||||
{
|
||||
activeSessionName = s.SessionId.ToString();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void OnClickClose()
|
||||
{
|
||||
show = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.shogi {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
padding-top: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@using Shogi.UI.Pages.Home.VisualAids
|
||||
@@ -1,74 +0,0 @@
|
||||
@page "/login"
|
||||
@inject IAccountManagement Acct
|
||||
@inject NavigationManager navigator
|
||||
|
||||
<main class="PrimaryTheme">
|
||||
<h1>Login</h1>
|
||||
|
||||
<section class="LoginForm">
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div>You're logged in as @context.User.Identity?.Name.</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
@if (errorList.Length > 0)
|
||||
{
|
||||
<ul class="Errors" style="grid-area: errors">
|
||||
@foreach (var error in errorList)
|
||||
{
|
||||
<li>@error</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
<label for="email" style="grid-area: emailLabel">Email</label>
|
||||
<input required id="email" name="emailInput" type="email" style="grid-area: emailControl" @bind-value="email" />
|
||||
|
||||
<label for="password" style="grid-area: passLabel">Password</label>
|
||||
<input required id="password" name="passwordInput" type="password" style="grid-area: passControl" @bind-value="password" />
|
||||
|
||||
<button style="grid-area: button" @onclick="DoLoginAsync">Login</button>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
@code {
|
||||
|
||||
private string email = string.Empty;
|
||||
private string password = string.Empty;
|
||||
private string[] errorList = [];
|
||||
|
||||
public async Task DoLoginAsync()
|
||||
{
|
||||
errorList = [];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
errorList = ["Email is required."];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
errorList = ["Password is required."];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Acct.LoginAsync(email, password);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
email = password = string.Empty;
|
||||
|
||||
navigator.NavigateTo("");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorList = result.ErrorList;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
main {
|
||||
/*display: grid;
|
||||
grid-template-areas:
|
||||
"header header header"
|
||||
". form ."
|
||||
". . .";
|
||||
grid-template-rows: auto 1fr 1fr;
|
||||
|
||||
place-items: center;
|
||||
*/
|
||||
}
|
||||
|
||||
.LoginForm {
|
||||
grid-area: form;
|
||||
|
||||
display: inline-grid;
|
||||
grid-template-areas:
|
||||
"errors errors"
|
||||
"emailLabel emailControl"
|
||||
"passLabel passControl"
|
||||
"button button";
|
||||
gap: 0.5rem 3rem;
|
||||
}
|
||||
|
||||
.LoginForm .Errors {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@using Contracts.Types;
|
||||
|
||||
<GameBoardPresentation Perspective="WhichPlayer.Player1" />
|
||||
|
||||
@code {
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
.game-board {
|
||||
--ratio: 0.9;
|
||||
display: grid;
|
||||
grid-template-columns: min-content repeat(9, minmax(2rem, 4rem)) max-content;
|
||||
grid-template-rows: repeat(9, 1fr) auto;
|
||||
background-color: #444;
|
||||
gap: 3px;
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
.board {
|
||||
display: grid;
|
||||
grid-column: span 10;
|
||||
grid-row: span 10;
|
||||
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: subgrid;
|
||||
grid-template-rows: subgrid;
|
||||
background-color: #444444;
|
||||
max-height: 100%;
|
||||
aspect-ratio: var(--ratio);
|
||||
}
|
||||
|
||||
.board[data-perspective="Player2"] {
|
||||
grid-template-areas:
|
||||
"rank 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"
|
||||
". file file file file file file file file file";
|
||||
}
|
||||
|
||||
|
||||
.tile {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
aspect-ratio: var(--ratio);
|
||||
background-color: beige;
|
||||
transition: filter linear 0.25s;
|
||||
}
|
||||
|
||||
.tile[data-selected] {
|
||||
filter: invert(0.8);
|
||||
}
|
||||
|
||||
.ruler {
|
||||
color: beige;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.ruler.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.board[data-perspective="Player2"] .ruler {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.board[data-perspective="Player2"] .ruler.vertical {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.side-board {
|
||||
grid-row: span 9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
place-content: space-between;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.side-board .player-area {
|
||||
display: grid;
|
||||
place-items: stretch;
|
||||
}
|
||||
|
||||
.side-board .hand {
|
||||
display: grid;
|
||||
border: 1px solid #ccc;
|
||||
grid-template-columns: repeat(auto-fill, 3rem);
|
||||
grid-template-rows: 3rem;
|
||||
place-items: center start;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 1000px) {
|
||||
.game-board {
|
||||
grid-template-columns: min-content repeat(9, minmax(2rem, 4rem));
|
||||
grid-template-rows: repeat(9, 1fr) auto max-content;
|
||||
}
|
||||
|
||||
.side-board {
|
||||
grid-row: unset;
|
||||
grid-column: span 10;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
@using Shogi.Contracts.Types
|
||||
|
||||
@inject ShogiApi Api
|
||||
|
||||
<gameBrowserEntry>
|
||||
@if (showDeletePrompt)
|
||||
{
|
||||
<modal class="PrimaryTheme ThemeVariant--Contrast">
|
||||
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
|
||||
@if (showDeleteError)
|
||||
{
|
||||
<p style="color: darkred;">An error occurred.</p>
|
||||
<div style="flex: 1;" />
|
||||
<button @onclick="HideModal">Cancel</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Do you wish to delete this session?</p>
|
||||
<div style="flex: 1;" />
|
||||
<button @onclick="HideModal">No</button>
|
||||
<button @onclick="DeleteSession">Yes</button>
|
||||
}
|
||||
</div>
|
||||
</modal>
|
||||
}
|
||||
|
||||
<div>
|
||||
<a href="play/@Session.SessionId">@Session.Player1</a>
|
||||
</div>
|
||||
@if (string.IsNullOrEmpty(Session.Player2))
|
||||
{
|
||||
<span>1 / 2</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Full</span>
|
||||
}
|
||||
<AuthorizeView>
|
||||
@if (context.User.Identity?.Name == Session.Player1)
|
||||
{
|
||||
<IconButton OnClick="() => showDeletePrompt = true">
|
||||
<TrashCanIcon />
|
||||
</IconButton>
|
||||
}
|
||||
</AuthorizeView>
|
||||
</gameBrowserEntry>
|
||||
|
||||
@code {
|
||||
[Parameter][EditorRequired] public SessionMetadata Session { get; set; } = default!;
|
||||
[Parameter][EditorRequired] public EventCallback OnSessionDeleted { get; set; }
|
||||
private bool showDeletePrompt = false;
|
||||
private bool showDeleteError = false;
|
||||
|
||||
|
||||
void HideModal()
|
||||
{
|
||||
showDeletePrompt = showDeleteError = false;
|
||||
}
|
||||
|
||||
async Task DeleteSession()
|
||||
{
|
||||
var response = await Api.DeleteSession(Session.SessionId);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
showDeletePrompt = false;
|
||||
showDeleteError = false;
|
||||
await OnSessionDeleted.InvokeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
showDeletePrompt = true;
|
||||
showDeleteError = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
::deep svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
[data-upsidedown] {
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
|
||||
.game-piece {
|
||||
overflow: hidden; /* Because SVGs have weird sizes. */
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
@page "/search"
|
||||
|
||||
<main class="SearchPage PrimaryTheme">
|
||||
<h3>Find Sessions</h3>
|
||||
|
||||
<GameBrowser />
|
||||
</main>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.SearchPage {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using Shogi.UI;
|
||||
using Shogi.UI.Identity;
|
||||
using Shogi.UI.Pages.Play;
|
||||
using Shogi.UI.Shared;
|
||||
using System.Text.Json;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
builder.Logging.AddConfiguration(
|
||||
builder.Configuration.GetSection("Logging"));
|
||||
ConfigureDependencies(builder.Services, builder.Configuration);
|
||||
|
||||
builder.Services.AddResponseCompression(options =>
|
||||
{
|
||||
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]);
|
||||
});
|
||||
|
||||
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 baseUrl = configuration["ShogiApiUrl"];
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
throw new InvalidOperationException("ShogiApiUrl configuration is missing.");
|
||||
}
|
||||
|
||||
var shogiApiUrl = new Uri(baseUrl, UriKind.Absolute);
|
||||
|
||||
services
|
||||
.AddTransient<CookieCredentialsMessageHandler>()
|
||||
.AddTransient<ILocalStorage, LocalStorage>();
|
||||
|
||||
// Identity
|
||||
services
|
||||
.AddAuthorizationCore(options => options.AddPolicy("Admin", policy => policy.RequireUserName("Hauth@live.com")))
|
||||
.AddScoped<AuthenticationStateProvider, CookieAuthenticationStateProvider>()
|
||||
.AddScoped(sp => (IAccountManagement)sp.GetRequiredService<AuthenticationStateProvider>())
|
||||
.AddHttpClient("Auth", client => client.BaseAddress = shogiApiUrl) // "Auth" is the name expected by the auth library.
|
||||
.AddHttpMessageHandler<CookieCredentialsMessageHandler>();
|
||||
|
||||
// Network clients
|
||||
services
|
||||
.AddHttpClient<ShogiApi>(client => client.BaseAddress = shogiApiUrl)
|
||||
.AddHttpMessageHandler<CookieCredentialsMessageHandler>();
|
||||
services
|
||||
.AddSingleton<GameHubNode>();
|
||||
|
||||
|
||||
var serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
services.AddSingleton((sp) => serializerOptions);
|
||||
}
|
||||
63
Shogi.UI/Properties/Resources.Designer.cs
generated
63
Shogi.UI/Properties/Resources.Designer.cs
generated
@@ -1,63 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 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.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Shogi.UI.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// 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() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 1.3
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">1.3</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1">this is my long string</data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
[base64 mime encoded serialized .NET Framework object]
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"identityapp1": {
|
||||
"type": "identityapp",
|
||||
"dynamicId": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"identityapp1": {
|
||||
"type": "identityapp.default",
|
||||
"dynamicId": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
using Shogi.Contracts.Api.Commands;
|
||||
using Shogi.Contracts.Types;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Shogi.UI.Shared;
|
||||
|
||||
public class ShogiApi(HttpClient httpClient)
|
||||
{
|
||||
private readonly JsonSerializerOptions serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task Register(string email, string password)
|
||||
{
|
||||
var response = await httpClient.PostAsJsonAsync(Relative("register"), new { email, password });
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task LoginEventArgs(string email, string password)
|
||||
{
|
||||
var response = await httpClient.PostAsJsonAsync("login?useCookies=true", new { email, password });
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task Logout()
|
||||
{
|
||||
var response = await httpClient.PutAsync(Relative("User/GuestLogout"), null);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<Session?> GetSession(string name)
|
||||
{
|
||||
var response = await httpClient.GetAsync(Relative($"Sessions/{name}"));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return (await response.Content.ReadFromJsonAsync<Session>(this.serializerOptions));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<SessionMetadata[]> GetAllSessionsMetadata()
|
||||
{
|
||||
var response = await httpClient.GetAsync(Relative("Sessions"));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return (await response.Content.ReadFromJsonAsync<SessionMetadata[]>(this.serializerOptions))!;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns false if the move was not accepted by the server.
|
||||
/// </summary>
|
||||
public async Task<bool> Move(Guid sessionName, MovePieceCommand command)
|
||||
{
|
||||
var response = await httpClient.PatchAsync(Relative($"Sessions/{sessionName}/Move"), JsonContent.Create(command));
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<string?> PostSession()
|
||||
{
|
||||
var response = await httpClient.PostAsync(Relative("Sessions"), null);
|
||||
var sessionId = response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : null;
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PatchJoinGame(string name)
|
||||
{
|
||||
return httpClient.PatchAsync(Relative($"Sessions/{name}/Join"), null);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> DeleteSession(Guid sessionId)
|
||||
{
|
||||
return httpClient.DeleteAsync(Relative($"Sessions/{sessionId}"));
|
||||
}
|
||||
|
||||
private static Uri Relative(string path) => new(path, UriKind.Relative);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="zzzNotSure\**" />
|
||||
<Content Remove="zzzNotSure\**" />
|
||||
<EmbeddedResource Remove="zzzNotSure\**" />
|
||||
<None Remove="zzzNotSure\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Pages\Home\VisualAids\PromotedPieceVisualAid.razor.css" />
|
||||
<None Remove="Pages\Play\GameBoard\PlayerName.razor.css" />
|
||||
<None Remove="Pages\Play\GameBrowserEntry.razor.css" />
|
||||
<None Remove="Pages\SearchPage.razor.css" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Pages\Home\VisualAids\PromotedPieceVisualAid.razor.css" />
|
||||
<Content Include="Pages\Play\GameBoard\PlayerName.razor.css" />
|
||||
<Content Include="Pages\Play\GameBrowserEntry.razor.css" />
|
||||
<Content Include="Pages\SearchPage.razor.css" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client.Core" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Shogi.Contracts\Shogi.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,2 +0,0 @@
|
||||
{
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Error",
|
||||
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information",
|
||||
"System.Net.Http.HttpClient": "Error",
|
||||
"Microsoft.AspNetCore.SignalR": "Debug",
|
||||
"Microsoft.AspNetCore.Http.Connections": "Debug"
|
||||
}
|
||||
},
|
||||
"ShogiApiUrl": "https://localhost:5001",
|
||||
"ShogiApiUrl2": "https://api.lucaserver.space/Shogi.Api/"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Shogi.UI</title>
|
||||
<script type="text/javascript">
|
||||
var base = document.createElement('base');
|
||||
base.href = window.location.href.includes("localhost")
|
||||
? "/"
|
||||
: "/shogi/";
|
||||
document.head.appendChild(base);
|
||||
</script>
|
||||
|
||||
<link href="css/app.css" rel="stylesheet" />
|
||||
<link href="css/themes.css" rel="stylesheet" />
|
||||
<link href="Shogi.UI.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
67
Shogi.sln
67
Shogi.sln
@@ -1,34 +1,27 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.3.11312.210
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
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}") = "UnitTests", "Tests\UnitTests\UnitTests.csproj", "{4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A968C8E6-47B7-4F72-A27A-AC9B643FD320}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.AcceptanceTests", "Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj", "{30F4E3DB-027F-4885-BE06-884167C1C6CF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Contracts", "Shogi.Contracts\Shogi.Contracts.csproj", "{1B9445A9-9C4D-46E3-8027-926C7FE0B55B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E69DE334-29A7-46AE-9647-54DC0187CD8D}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
azure-pipelines.yml = azure-pipelines.yml
|
||||
Shogi\.config\dotnet-tools.json = Shogi\.config\dotnet-tools.json
|
||||
global.json = global.json
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.UI", "Shogi.UI\Shogi.UI.csproj", "{12B90F81-AFE6-4CD5-8517-218C0D70A1B6}"
|
||||
EndProject
|
||||
Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Shogi.Database", "Shogi.Database\Shogi.Database.sqlproj", "{9B115B71-088F-41EF-858F-C7B155271A9F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Api", "Shogi.Api\Shogi.Api.csproj", "{62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoardRules", "BoardRules\BoardRules.csproj", "{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "E2ETests", "Tests\E2ETests\E2ETests.csproj", "{401120C3-45D6-4A23-8D87-C2BED29F4950}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{20DA20BB-85F1-4DBE-9B22-3C4FAF89647B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "Tests\UnitTests\UnitTests.csproj", "{9D1DD2CD-7B04-4472-4377-027563F356CA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi", "Shogi\Shogi.csproj", "{E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -36,45 +29,29 @@ Global
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{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
|
||||
{4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{30F4E3DB-027F-4885-BE06-884167C1C6CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{30F4E3DB-027F-4885-BE06-884167C1C6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{30F4E3DB-027F-4885-BE06-884167C1C6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1B9445A9-9C4D-46E3-8027-926C7FE0B55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1B9445A9-9C4D-46E3-8027-926C7FE0B55B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1B9445A9-9C4D-46E3-8027-926C7FE0B55B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1B9445A9-9C4D-46E3-8027-926C7FE0B55B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{12B90F81-AFE6-4CD5-8517-218C0D70A1B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{12B90F81-AFE6-4CD5-8517-218C0D70A1B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{12B90F81-AFE6-4CD5-8517-218C0D70A1B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{12B90F81-AFE6-4CD5-8517-218C0D70A1B6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9B115B71-088F-41EF-858F-C7B155271A9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9B115B71-088F-41EF-858F-C7B155271A9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9B115B71-088F-41EF-858F-C7B155271A9F}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
{9B115B71-088F-41EF-858F-C7B155271A9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9B115B71-088F-41EF-858F-C7B155271A9F}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{401120C3-45D6-4A23-8D87-C2BED29F4950}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{401120C3-45D6-4A23-8D87-C2BED29F4950}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{401120C3-45D6-4A23-8D87-C2BED29F4950}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{401120C3-45D6-4A23-8D87-C2BED29F4950}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9D1DD2CD-7B04-4472-4377-027563F356CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9D1DD2CD-7B04-4472-4377-027563F356CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9D1DD2CD-7B04-4472-4377-027563F356CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9D1DD2CD-7B04-4472-4377-027563F356CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E6BEF2A0-4372-D199-EF2D-F92A890DBC3A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320}
|
||||
{30F4E3DB-027F-4885-BE06-884167C1C6CF} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320}
|
||||
{401120C3-45D6-4A23-8D87-C2BED29F4950} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320}
|
||||
{9D1DD2CD-7B04-4472-4377-027563F356CA} = {20DA20BB-85F1-4DBE-9B22-3C4FAF89647B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "6.0.5",
|
||||
"version": "10.0.2",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Shogi.Api;
|
||||
namespace Shogi;
|
||||
|
||||
public class ApiKeys
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Shogi.Api.Application;
|
||||
namespace Shogi.BackEnd.Application;
|
||||
|
||||
/// <summary>
|
||||
/// Used to receive signals from connected clients.
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Shogi.Api.Application;
|
||||
namespace Shogi.BackEnd.Application;
|
||||
|
||||
/// <summary>
|
||||
/// Used to send signals to connected clients.
|
||||
@@ -1,15 +1,14 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Shogi.Api.Controllers;
|
||||
using Shogi.Api.Extensions;
|
||||
using Shogi.Api.Identity;
|
||||
using Shogi.Api.Repositories;
|
||||
using Shogi.Api.Repositories.Dto;
|
||||
using Shogi.Contracts.Api.Commands;
|
||||
using Shogi.Domain.Aggregates;
|
||||
using Shogi.BackEnd.Controllers;
|
||||
using Shogi.BackEnd.Extensions;
|
||||
using Shogi.BackEnd.Identity;
|
||||
using Shogi.BackEnd.Repositories;
|
||||
using Shogi.BackEnd.Repositories.Dto;
|
||||
using Shogi.BackEnd.Domains.Aggregates;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
namespace Shogi.Api.Application;
|
||||
namespace Shogi.BackEnd.Application;
|
||||
|
||||
public class ShogiApplication(
|
||||
QueryRepository queryRepository,
|
||||
@@ -72,10 +71,10 @@ public class ShogiApplication(
|
||||
return session;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> MovePiece(string playerId, string sessionId, MovePieceCommand command)
|
||||
public async Task<IActionResult> MovePiece(string playerId, string sessionId, Types.MovePieceCommand command)
|
||||
{
|
||||
var session = await this.ReadSession(sessionId);
|
||||
if (session == null)
|
||||
if (session is null)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
@@ -105,7 +104,7 @@ public class ShogiApplication(
|
||||
public async Task<IActionResult> JoinSession(string sessionId, string player2Id)
|
||||
{
|
||||
var session = await this.ReadSession(sessionId);
|
||||
if (session == null) return new NotFoundResult();
|
||||
if (session is null) return new NotFoundResult();
|
||||
|
||||
if (string.IsNullOrEmpty(session.Player2))
|
||||
{
|
||||
@@ -1,18 +1,21 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Shogi.Api.Identity;
|
||||
using Shogi.BackEnd.Identity;
|
||||
using Shogi.BackEnd.Repositories;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Shogi.Api.Controllers;
|
||||
namespace Shogi.BackEnd.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[Route("[controller]")]
|
||||
[Route("backend/[controller]")]
|
||||
[ApiController]
|
||||
public class AccountController(
|
||||
SignInManager<ShogiUser> signInManager,
|
||||
UserManager<ShogiUser> UserManager,
|
||||
IConfiguration configuration) : ControllerBase
|
||||
IConfiguration configuration,
|
||||
SessionRepository sessionRepository,
|
||||
QueryRepository queryRepository) : ControllerBase
|
||||
{
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("TestAccount")]
|
||||
@@ -36,21 +39,53 @@ public class AccountController(
|
||||
return this.Created();
|
||||
}
|
||||
|
||||
[HttpPost("/logout")]
|
||||
public async Task<IActionResult> Logout([FromBody] object empty)
|
||||
[Authorize("Admin")]
|
||||
[HttpDelete("TestAccount")]
|
||||
public async Task<IActionResult> DeleteTestAccounts()
|
||||
{
|
||||
// https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support
|
||||
if (empty is not null)
|
||||
{
|
||||
await signInManager.SignOutAsync();
|
||||
var testUsers = new[] { "aat-account", "aat-account-2" };
|
||||
|
||||
return this.Ok();
|
||||
foreach (var username in testUsers)
|
||||
{
|
||||
var user = await UserManager.FindByNameAsync(username);
|
||||
if (user != null)
|
||||
{
|
||||
var sessions = await queryRepository.ReadSessionsMetadata(user.Id);
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
await sessionRepository.DeleteSession(session.Id);
|
||||
}
|
||||
|
||||
await UserManager.DeleteAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
return this.Unauthorized();
|
||||
return this.NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("/roles")]
|
||||
[HttpPost("Login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login([FromForm] string email, [FromForm] string password)
|
||||
{
|
||||
var result = await signInManager.PasswordSignInAsync(email, password, isPersistent: true, lockoutOnFailure: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
return Redirect("/login?error=Invalid login attempt.");
|
||||
}
|
||||
|
||||
[HttpGet("Logout")]
|
||||
[HttpPost("Logout")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await signInManager.SignOutAsync();
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
[HttpGet("/backend/roles")]
|
||||
public IActionResult GetRoles()
|
||||
{
|
||||
if (this.User.Identity is not null && this.User.Identity.IsAuthenticated)
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Shogi.Api.Controllers;
|
||||
namespace Shogi.BackEnd.Controllers;
|
||||
|
||||
public static class Extentions
|
||||
{
|
||||
@@ -15,7 +15,7 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace Shogi.Api.Controllers;
|
||||
namespace Shogi.BackEnd.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add identity endpoints.
|
||||
@@ -47,7 +47,7 @@ public static class MyIdentityApiEndpointRouteBuilderExtensions
|
||||
// We'll figure out a unique endpoint name based on the final route pattern during endpoint generation.
|
||||
string? confirmEmailEndpointName = null;
|
||||
|
||||
var routeGroup = endpoints.MapGroup("");
|
||||
var routeGroup = endpoints.MapGroup("backend");
|
||||
|
||||
// NOTE: We cannot inject UserManager<TUser> directly because the TUser generic parameter is currently unsupported by RDG.
|
||||
// https://github.com/dotnet/aspnetcore/issues/47338
|
||||
@@ -407,7 +407,7 @@ public static class MyIdentityApiEndpointRouteBuilderExtensions
|
||||
routeValues.Add("changedEmail", email);
|
||||
}
|
||||
|
||||
HostString? host = environment.IsDevelopment() ? null : new HostString("api.lucaserver.space/Shogi.Api");
|
||||
HostString? host = environment.IsDevelopment() ? null : new HostString("api.lucaserver.space/Shogi");
|
||||
var confirmEmailUrl = linkGenerator.GetUriByName(context, confirmEmailEndpointName, routeValues, host: host)
|
||||
?? throw new NotSupportedException($"Could not find endpoint named '{confirmEmailEndpointName}'.");
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Shogi.Api.Application;
|
||||
using Shogi.Api.Extensions;
|
||||
using Shogi.Api.Repositories;
|
||||
using Shogi.Contracts.Api.Commands;
|
||||
using Shogi.Contracts.Types;
|
||||
using Shogi.BackEnd.Application;
|
||||
using Shogi.BackEnd.Extensions;
|
||||
using Shogi.BackEnd.Repositories;
|
||||
using Shogi.BackEnd.Types;
|
||||
|
||||
namespace Shogi.Api.Controllers;
|
||||
namespace Shogi.BackEnd.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
[Route("backend/[controller]")]
|
||||
public class SessionsController(
|
||||
SessionRepository sessionRepository,
|
||||
ShogiApplication application) : ControllerBase
|
||||
@@ -57,25 +56,25 @@ public class SessionsController(
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<Session>> GetSession(Guid sessionId)
|
||||
{
|
||||
var session = await application.ReadSession(sessionId.ToString());
|
||||
if (session == null) return this.NotFound();
|
||||
var domainSession = await application.ReadSession(sessionId.ToString());
|
||||
if (domainSession is null) return this.NotFound();
|
||||
|
||||
return new Session
|
||||
{
|
||||
BoardState = new BoardState
|
||||
{
|
||||
Board = session.Board.BoardState.State.ToContract(),
|
||||
Player1Hand = session.Board.BoardState.Player1Hand.ToContract(),
|
||||
Player2Hand = session.Board.BoardState.Player2Hand.ToContract(),
|
||||
PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(),
|
||||
WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract(),
|
||||
Victor = session.Board.BoardState.IsCheckmate
|
||||
? session.Board.BoardState.InCheck == Domain.ValueObjects.WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1
|
||||
Board = domainSession.Board.BoardState.State.ToContract(),
|
||||
Player1Hand = domainSession.Board.BoardState.Player1Hand.ToContract(),
|
||||
Player2Hand = domainSession.Board.BoardState.Player2Hand.ToContract(),
|
||||
PlayerInCheck = domainSession.Board.BoardState.InCheck?.ToContract(),
|
||||
WhoseTurn = domainSession.Board.BoardState.WhoseTurn.ToContract(),
|
||||
Victor = domainSession.Board.BoardState.IsCheckmate
|
||||
? domainSession.Board.BoardState.InCheck == Domains.ValueObjects.WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1
|
||||
: null
|
||||
},
|
||||
Player1 = application.GetUsername(session.Player1),
|
||||
Player2 = application.GetUsername(session.Player2),
|
||||
SessionId = session.Id
|
||||
Player1 = application.GetUsername(domainSession.Player1),
|
||||
Player2 = application.GetUsername(domainSession.Player2),
|
||||
SessionId = domainSession.Id
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using Shogi.BackEnd.Domains.ValueObjects;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Rules;
|
||||
|
||||
namespace Shogi.Domain.Aggregates;
|
||||
namespace Shogi.BackEnd.Domains.Aggregates;
|
||||
|
||||
public class Session(Guid id, string player1Name)
|
||||
{
|
||||
@@ -1,8 +1,11 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD;
|
||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Numerics;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
using Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD;
|
||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.BackEnd.Domains.ValueObjects.Pieces.Piece>;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects;
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects;
|
||||
|
||||
public class BoardState
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Shogi.Domain.ValueObjects;
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects;
|
||||
|
||||
[Flags]
|
||||
internal enum InCheckResult
|
||||
@@ -32,3 +32,9 @@ public enum WhichPiece
|
||||
Pawn,
|
||||
//PromotedPawn,
|
||||
}
|
||||
|
||||
public enum WhichPlayer
|
||||
{
|
||||
Player1,
|
||||
Player2
|
||||
}
|
||||
17
Shogi/BackEnd/Domains/ValueObjects/Movement/Direction.cs
Normal file
17
Shogi/BackEnd/Domains/ValueObjects/Movement/Direction.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
|
||||
public static class Direction
|
||||
{
|
||||
public static readonly Vector2 Forward = new(0, 1);
|
||||
public static readonly Vector2 Backward = new(0, -1);
|
||||
public static readonly Vector2 Left = new(-1, 0);
|
||||
public static readonly Vector2 Right = new(1, 0);
|
||||
public static readonly Vector2 ForwardLeft = new(-1, 1);
|
||||
public static readonly Vector2 ForwardRight = new(1, 1);
|
||||
public static readonly Vector2 BackwardLeft = new(-1, -1);
|
||||
public static readonly Vector2 BackwardRight = new(1, -1);
|
||||
public static readonly Vector2 KnightLeft = new(-1, 2);
|
||||
public static readonly Vector2 KnightRight = new(1, 2);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
|
||||
public enum Distance
|
||||
{
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Shogi.Domain.ValueObjects;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single piece being moved by a player from <paramref name="From"/> to <paramref name="To"/>.
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
|
||||
public record MoveResult(bool IsSuccess, string Reason = "")
|
||||
{
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
|
||||
[DebuggerDisplay("{Step} - {Distance}")]
|
||||
public record Path
|
||||
@@ -17,7 +18,7 @@ public record Path
|
||||
public Path(Vector2 step, Distance distance = Distance.OneStep)
|
||||
{
|
||||
Step = step;
|
||||
this.Distance = distance;
|
||||
Distance = distance;
|
||||
}
|
||||
|
||||
public Path Invert() => new(Vector2.Negate(Step), Distance);
|
||||
47
Shogi/BackEnd/Domains/ValueObjects/Pieces/Bishop.cs
Normal file
47
Shogi/BackEnd/Domains/ValueObjects/Pieces/Bishop.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
internal record class Bishop : Piece
|
||||
{
|
||||
private static readonly ReadOnlyCollection<Path> BishopPaths = new(new List<Path>(4)
|
||||
{
|
||||
new Path(Direction.ForwardLeft, Distance.MultiStep),
|
||||
new Path(Direction.ForwardRight, Distance.MultiStep),
|
||||
new Path(Direction.BackwardLeft, Distance.MultiStep),
|
||||
new Path(Direction.BackwardRight, Distance.MultiStep)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> PromotedBishopPaths = new(new List<Path>(8)
|
||||
{
|
||||
new Path(Direction.Forward),
|
||||
new Path(Direction.Left),
|
||||
new Path(Direction.Right),
|
||||
new Path(Direction.Backward),
|
||||
new Path(Direction.ForwardLeft, Distance.MultiStep),
|
||||
new Path(Direction.ForwardRight, Distance.MultiStep),
|
||||
new Path(Direction.BackwardLeft, Distance.MultiStep),
|
||||
new Path(Direction.BackwardRight, Distance.MultiStep)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
BishopPaths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2PromotedPaths =
|
||||
PromotedBishopPaths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Bishop(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Bishop, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<Path> MoveSet => IsPromoted ? PromotedBishopPaths : BishopPaths;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects;
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
internal record class GoldGeneral : Piece
|
||||
{
|
||||
@@ -1,7 +1,8 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects;
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
internal record class King : Piece
|
||||
{
|
||||
32
Shogi/BackEnd/Domains/ValueObjects/Pieces/Knight.cs
Normal file
32
Shogi/BackEnd/Domains/ValueObjects/Pieces/Knight.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
internal record class Knight : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(2)
|
||||
{
|
||||
new Path(Direction.KnightLeft),
|
||||
new Path(Direction.KnightRight)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Knight(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Knight, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
31
Shogi/BackEnd/Domains/ValueObjects/Pieces/Lance.cs
Normal file
31
Shogi/BackEnd/Domains/ValueObjects/Pieces/Lance.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
internal record class Lance : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
||||
{
|
||||
new Path(Direction.Forward, Distance.MultiStep),
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Lance(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Lance, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
31
Shogi/BackEnd/Domains/ValueObjects/Pieces/Pawn.cs
Normal file
31
Shogi/BackEnd/Domains/ValueObjects/Pieces/Pawn.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
internal record class Pawn : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(1)
|
||||
{
|
||||
new Path(Direction.Forward)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public Pawn(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.Pawn, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
100
Shogi/BackEnd/Domains/ValueObjects/Pieces/Piece.cs
Normal file
100
Shogi/BackEnd/Domains/ValueObjects/Pieces/Piece.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
[DebuggerDisplay("{WhichPiece} {Owner}")]
|
||||
public abstract record class Piece
|
||||
{
|
||||
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<Path> MoveSet { get; }
|
||||
public WhichPiece WhichPiece { get; }
|
||||
public WhichPlayer Owner { get; private set; }
|
||||
public bool IsPromoted { get; private set; }
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Prep the piece for capture by changing ownership and demoting.
|
||||
/// </summary>
|
||||
public void Capture(WhichPlayer newOwner)
|
||||
{
|
||||
Owner = newOwner;
|
||||
IsPromoted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Respecting the move-set of the Piece, collect all positions along the shortest path from start to end.
|
||||
/// Useful if you need to iterate a move-set.
|
||||
/// </summary>
|
||||
/// <param name="start"></param>
|
||||
/// <param name="end"></param>
|
||||
/// <returns>An empty list if the piece cannot legally traverse from start to end. Otherwise, a list of positions.</returns>
|
||||
public IEnumerable<Vector2> GetPathFromStartToEnd(Vector2 start, Vector2 end)
|
||||
{
|
||||
var steps = new List<Vector2>(10);
|
||||
|
||||
var path = GetNearestPath(MoveSet, start, end);
|
||||
var position = start;
|
||||
while (Vector2.Distance(start, position) < Vector2.Distance(start, end))
|
||||
{
|
||||
position += path.Step;
|
||||
steps.Add(position);
|
||||
|
||||
if (path.Distance == Distance.OneStep) break;
|
||||
}
|
||||
|
||||
if (position == end)
|
||||
{
|
||||
return steps;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static Path GetNearestPath(IEnumerable<Path> paths, Vector2 start, Vector2 end)
|
||||
{
|
||||
if (!paths.DefaultIfEmpty().Any())
|
||||
{
|
||||
throw new ArgumentException("No paths to get nearest path from.");
|
||||
}
|
||||
|
||||
var shortestPath = paths.First();
|
||||
foreach (var path in paths.Skip(1))
|
||||
{
|
||||
var distance = Vector2.Distance(start + path.Step, end);
|
||||
var shortestDistance = Vector2.Distance(start + shortestPath.Step, end);
|
||||
if (distance < shortestDistance)
|
||||
{
|
||||
shortestPath = path;
|
||||
}
|
||||
}
|
||||
return shortestPath;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.Domain.ValueObjects;
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
public record class Rook : Piece
|
||||
{
|
||||
35
Shogi/BackEnd/Domains/ValueObjects/Pieces/SilverGeneral.cs
Normal file
35
Shogi/BackEnd/Domains/ValueObjects/Pieces/SilverGeneral.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
internal record class SilverGeneral : Piece
|
||||
{
|
||||
public static readonly ReadOnlyCollection<Path> Player1Paths = new(new List<Path>(4)
|
||||
{
|
||||
new Path(Direction.Forward),
|
||||
new Path(Direction.ForwardLeft),
|
||||
new Path(Direction.ForwardRight),
|
||||
new Path(Direction.BackwardLeft),
|
||||
new Path(Direction.BackwardRight)
|
||||
});
|
||||
|
||||
public static readonly ReadOnlyCollection<Path> Player2Paths =
|
||||
Player1Paths
|
||||
.Select(p => p.Invert())
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
public SilverGeneral(WhichPlayer owner, bool isPromoted = false)
|
||||
: base(WhichPiece.SilverGeneral, owner, isPromoted)
|
||||
{
|
||||
}
|
||||
|
||||
public override ReadOnlyCollection<Path> MoveSet => Owner switch
|
||||
{
|
||||
WhichPlayer.Player1 => IsPromoted ? GoldGeneral.Player1Paths : Player1Paths,
|
||||
WhichPlayer.Player2 => IsPromoted ? GoldGeneral.Player2Paths : Player2Paths,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
using Shogi.Domain.YetToBeAssimilatedIntoDDD;
|
||||
namespace Shogi.Domain.ValueObjects;
|
||||
using System.Numerics;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Movement;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
using Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD;
|
||||
using Path = Shogi.BackEnd.Domains.ValueObjects.Movement.Path;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Facilitates Shogi board state transitions, cognisant of Shogi rules.
|
||||
@@ -247,7 +252,7 @@ public sealed class ShogiBoard(BoardState initialState)
|
||||
{
|
||||
var list = new List<Vector2>(10);
|
||||
var position = path.Step + piecePosition;
|
||||
if (path.Distance == YetToBeAssimilatedIntoDDD.Pathing.Distance.MultiStep)
|
||||
if (path.Distance == Distance.MultiStep)
|
||||
{
|
||||
|
||||
while (position.IsInsideBoardBoundary())
|
||||
@@ -340,7 +345,7 @@ public sealed class ShogiBoard(BoardState initialState)
|
||||
else
|
||||
{
|
||||
var multiStepPaths = matchingPaths
|
||||
.Where(path => path.Distance == YetToBeAssimilatedIntoDDD.Pathing.Distance.MultiStep)
|
||||
.Where(path => path.Distance == Distance.MultiStep)
|
||||
.ToArray();
|
||||
if (multiStepPaths.Length == 0)
|
||||
{
|
||||
@@ -371,7 +376,7 @@ public sealed class ShogiBoard(BoardState initialState)
|
||||
return new MoveResult(true);
|
||||
}
|
||||
|
||||
private static IEnumerable<Vector2> GetPositionsAlongPath(Vector2 from, Vector2 to, YetToBeAssimilatedIntoDDD.Pathing.Path path)
|
||||
private static IEnumerable<Vector2> GetPositionsAlongPath(Vector2 from, Vector2 to, Path path)
|
||||
{
|
||||
var next = from;
|
||||
while (next != to && next.X >= 0 && next.X < 9 && next.Y >= 0 && next.Y < 9)
|
||||
166
Shogi/BackEnd/Domains/ValueObjects/Rules/StandardRules.cs
Normal file
166
Shogi/BackEnd/Domains/ValueObjects/Rules/StandardRules.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD;
|
||||
using System.Numerics;
|
||||
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.BackEnd.Domains.ValueObjects.Pieces.Piece>;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.ValueObjects.Rules;
|
||||
|
||||
internal class StandardRules
|
||||
{
|
||||
private readonly BoardState boardState;
|
||||
|
||||
internal StandardRules(BoardState board)
|
||||
{
|
||||
boardState = board;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the last move put the player who moved in check.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This strategy recognizes that a "discover check" could only occur from a subset of pieces: Rook, Bishop, Lance.
|
||||
/// In this way, only those pieces need to be considered when evaluating if a move placed the moving player in check.
|
||||
/// </remarks>
|
||||
internal bool DidPlayerPutThemselfInCheck()
|
||||
{
|
||||
if (boardState.PreviousMove.From == null)
|
||||
{
|
||||
// You can't place yourself in check by placing a piece from your hand.
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousMovedPiece = boardState[boardState.PreviousMove.To];
|
||||
if (previousMovedPiece == null) throw new ArgumentNullException(nameof(previousMovedPiece), $"No piece exists at position {boardState.PreviousMove.To}.");
|
||||
var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player1KingPosition : boardState.Player2KingPosition;
|
||||
|
||||
|
||||
var isDiscoverCheck = false;
|
||||
// Get line equation from king through the now-unoccupied location.
|
||||
var direction = Vector2.Subtract(kingPosition, boardState.PreviousMove.From.Value);
|
||||
var slope = Math.Abs(direction.Y / direction.X);
|
||||
var path = BoardState.GetPathAlongDirectionFromStartToEdgeOfBoard(boardState.PreviousMove.From.Value, Vector2.Normalize(direction));
|
||||
var threat = boardState.QueryFirstPieceInPath(path);
|
||||
if (threat == null || threat.Owner == previousMovedPiece.Owner) return false;
|
||||
// If absolute slope is 45°, look for a bishop along the line.
|
||||
// If absolute slope is 0° or 90°, look for a rook along the line.
|
||||
// if absolute slope is 0°, look for lance along the line.
|
||||
if (float.IsInfinity(slope))
|
||||
{
|
||||
isDiscoverCheck = threat.WhichPiece switch
|
||||
{
|
||||
WhichPiece.Lance => !threat.IsPromoted,
|
||||
WhichPiece.Rook => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
else if (slope == 1)
|
||||
{
|
||||
isDiscoverCheck = threat.WhichPiece switch
|
||||
{
|
||||
WhichPiece.Bishop => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
else if (slope == 0)
|
||||
{
|
||||
isDiscoverCheck = threat.WhichPiece switch
|
||||
{
|
||||
WhichPiece.Rook => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
return isDiscoverCheck;
|
||||
}
|
||||
|
||||
internal bool IsOpponentInCheckAfterMove() => IsOpposingKingThreatenedByPosition(boardState.PreviousMove.To);
|
||||
|
||||
internal bool IsOpposingKingThreatenedByPosition(Vector2 position)
|
||||
{
|
||||
var previousMovedPiece = boardState[position];
|
||||
if (previousMovedPiece == null) return false;
|
||||
|
||||
var kingPosition = previousMovedPiece.Owner == WhichPlayer.Player1 ? boardState.Player2KingPosition : boardState.Player1KingPosition;
|
||||
var path = previousMovedPiece.GetPathFromStartToEnd(position, kingPosition);
|
||||
var threatenedPiece = boardState.QueryFirstPieceInPath(path);
|
||||
if (!path.Any() || threatenedPiece == null) return false;
|
||||
|
||||
return threatenedPiece.WhichPiece == WhichPiece.King;
|
||||
}
|
||||
|
||||
internal bool IsOpponentInCheckMate()
|
||||
{
|
||||
// Assume checkmate, then try to disprove.
|
||||
if (!boardState.InCheck.HasValue) return false;
|
||||
// Get all pieces from opponent who threaten the king in question.
|
||||
var opponent = boardState.WhoseTurn == WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||
var tilesOccupiedByOpponent = boardState.GetTilesOccupiedBy(opponent);
|
||||
var kingPosition = boardState.WhoseTurn == WhichPlayer.Player1
|
||||
? boardState.Player1KingPosition
|
||||
: boardState.Player2KingPosition;
|
||||
var threats = tilesOccupiedByOpponent.Where(tile => PieceHasLineOfSight(tile, kingPosition)).ToList();
|
||||
if (threats.Count == 1)
|
||||
{
|
||||
/* If there is exactly one threat it is possible to block the check.
|
||||
* Foreach piece owned by whichPlayer
|
||||
* if piece can intercept check, return false;
|
||||
*/
|
||||
var threat = threats.Single();
|
||||
var pathFromThreatToKing = threat.Value.GetPathFromStartToEnd(threat.Key, kingPosition);
|
||||
var tilesThatCouldBlockTheThreat = boardState.GetTilesOccupiedBy(boardState.WhoseTurn);
|
||||
foreach (var threatBlockingPosition in pathFromThreatToKing)
|
||||
{
|
||||
var tilesThatDoBlockThreat = tilesThatCouldBlockTheThreat
|
||||
.Where(tile => PieceHasLineOfSight(tile, threatBlockingPosition))
|
||||
.ToList();
|
||||
|
||||
if (tilesThatDoBlockThreat.Any())
|
||||
{
|
||||
return false; // Cannot be check-mate if a piece can intercept the threat.
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* If no ability to block the check, maybe the king can evade check by moving.
|
||||
*/
|
||||
|
||||
foreach (var maybeSafePosition in GetPossiblePositionsForKing(boardState.WhoseTurn))
|
||||
{
|
||||
threats = tilesOccupiedByOpponent
|
||||
.Where(tile => PieceHasLineOfSight(tile, maybeSafePosition))
|
||||
.ToList();
|
||||
if (!threats.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Vector2> GetPossiblePositionsForKing(WhichPlayer whichPlayer)
|
||||
{
|
||||
var kingPosition = whichPlayer == WhichPlayer.Player1
|
||||
? boardState.Player1KingPosition
|
||||
: boardState.Player2KingPosition;
|
||||
|
||||
var paths = boardState[kingPosition]!.MoveSet;
|
||||
return paths
|
||||
.Select(path => path.Step + kingPosition)
|
||||
// Because the king could be on the edge of the board, where some of its paths do not make sense.
|
||||
.Where(newPosition => newPosition.IsInsideBoardBoundary())
|
||||
// Where tile at position is empty, meaning the king could move there.
|
||||
.Where(newPosition => boardState[newPosition] == null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private bool PieceHasLineOfSight(BoardTile tile, Vector2 lineOfSightTarget)
|
||||
{
|
||||
var path = tile.Value.GetPathFromStartToEnd(tile.Key, lineOfSightTarget);
|
||||
return path
|
||||
.SkipLast(1)
|
||||
.All(position => boardState[Notation.ToBoardNotation(position)] == null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Shogi.BackEnd.Domains.ValueObjects;
|
||||
using Shogi.BackEnd.Domains.ValueObjects.Pieces;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD;
|
||||
|
||||
internal static class DomainExtensions
|
||||
{
|
||||
public static bool IsKing(this Piece self) => self.WhichPiece == WhichPiece.King;
|
||||
|
||||
public static bool IsBetween(this float self, float min, float max)
|
||||
{
|
||||
return self >= min && self <= max;
|
||||
}
|
||||
|
||||
public static bool IsInsideBoardBoundary(this System.Numerics.Vector2 self)
|
||||
{
|
||||
return self.X.IsBetween(0, 8) && self.Y.IsBetween(0, 8);
|
||||
}
|
||||
}
|
||||
33
Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/Notation.cs
Normal file
33
Shogi/BackEnd/Domains/YetToBeAssimilatedIntoDDD/Notation.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Numerics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Shogi.BackEnd.Domains.YetToBeAssimilatedIntoDDD;
|
||||
|
||||
public static class Notation
|
||||
{
|
||||
private static readonly string BoardNotationRegex = @"(?<file>[A-I])(?<rank>[1-9])";
|
||||
private static readonly char A = 'A';
|
||||
|
||||
public static string ToBoardNotation(Vector2 vector)
|
||||
{
|
||||
return ToBoardNotation((int)vector.X, (int)vector.Y);
|
||||
}
|
||||
|
||||
public static string ToBoardNotation(int x, int y)
|
||||
{
|
||||
var file = (char)(x + A);
|
||||
var rank = y + 1;
|
||||
return $"{file}{rank}";
|
||||
}
|
||||
public static Vector2 FromBoardNotation(string notation)
|
||||
{
|
||||
if (Regex.IsMatch(notation, BoardNotationRegex))
|
||||
{
|
||||
var match = Regex.Match(notation, BoardNotationRegex, RegexOptions.IgnoreCase);
|
||||
char file = match.Groups["file"].Value[0];
|
||||
int rank = int.Parse(match.Groups["rank"].Value);
|
||||
return new Vector2(file - A, rank - 1);
|
||||
}
|
||||
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
|
||||
}
|
||||
}
|
||||
68
Shogi/BackEnd/Extensions/ContractsExtensions.cs
Normal file
68
Shogi/BackEnd/Extensions/ContractsExtensions.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Shogi.BackEnd.Types;
|
||||
using System.Collections.ObjectModel;
|
||||
using DomainValueObjects = Shogi.BackEnd.Domains.ValueObjects;
|
||||
|
||||
namespace Shogi.BackEnd.Extensions;
|
||||
|
||||
public static class ContractsExtensions
|
||||
{
|
||||
public static WhichPlayer ToContract(this DomainValueObjects.WhichPlayer player)
|
||||
{
|
||||
return player switch
|
||||
{
|
||||
DomainValueObjects.WhichPlayer.Player1 => WhichPlayer.Player1,
|
||||
DomainValueObjects.WhichPlayer.Player2 => WhichPlayer.Player2,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static WhichPiece ToContract(this DomainValueObjects.WhichPiece piece)
|
||||
{
|
||||
return piece switch
|
||||
{
|
||||
DomainValueObjects.WhichPiece.King => WhichPiece.King,
|
||||
DomainValueObjects.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral,
|
||||
DomainValueObjects.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral,
|
||||
DomainValueObjects.WhichPiece.Bishop => WhichPiece.Bishop,
|
||||
DomainValueObjects.WhichPiece.Rook => WhichPiece.Rook,
|
||||
DomainValueObjects.WhichPiece.Knight => WhichPiece.Knight,
|
||||
DomainValueObjects.WhichPiece.Lance => WhichPiece.Lance,
|
||||
DomainValueObjects.WhichPiece.Pawn => WhichPiece.Pawn,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static Piece ToContract(this DomainValueObjects.Pieces.Piece piece) => new()
|
||||
{
|
||||
IsPromoted = piece.IsPromoted,
|
||||
Owner = piece.Owner.ToContract(),
|
||||
WhichPiece = piece.WhichPiece.ToContract()
|
||||
};
|
||||
|
||||
public static IReadOnlyList<Piece> ToContract(this List<DomainValueObjects.Pieces.Piece> pieces)
|
||||
{
|
||||
return pieces
|
||||
.Select(ToContract)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
public static Dictionary<string, Piece?> ToContract(this ReadOnlyDictionary<string, DomainValueObjects.Pieces.Piece?> boardState) =>
|
||||
boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract());
|
||||
|
||||
public static DomainValueObjects.WhichPiece ToDomain(this WhichPiece piece)
|
||||
{
|
||||
return piece switch
|
||||
{
|
||||
WhichPiece.King => DomainValueObjects.WhichPiece.King,
|
||||
WhichPiece.GoldGeneral => DomainValueObjects.WhichPiece.GoldGeneral,
|
||||
WhichPiece.SilverGeneral => DomainValueObjects.WhichPiece.SilverGeneral,
|
||||
WhichPiece.Bishop => DomainValueObjects.WhichPiece.Bishop,
|
||||
WhichPiece.Rook => DomainValueObjects.WhichPiece.Rook,
|
||||
WhichPiece.Knight => DomainValueObjects.WhichPiece.Knight,
|
||||
WhichPiece.Lance => DomainValueObjects.WhichPiece.Lance,
|
||||
WhichPiece.Pawn => DomainValueObjects.WhichPiece.Pawn,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Shogi.Api.Identity;
|
||||
namespace Shogi.BackEnd.Identity;
|
||||
|
||||
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ShogiUser>(options)
|
||||
{
|
||||
18
Shogi/BackEnd/Identity/ApplicationDbContextFactory.cs
Normal file
18
Shogi/BackEnd/Identity/ApplicationDbContextFactory.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace Shogi.BackEnd.Identity;
|
||||
|
||||
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
|
||||
{
|
||||
public ApplicationDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
|
||||
// This connection string is strictly for design-time tools (like creating migrations).
|
||||
// It is NOT used when running the app or when applying the migration bundle.
|
||||
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=ShogiDb;Trusted_Connection=True;MultipleActiveResultSets=true");
|
||||
|
||||
return new ApplicationDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
72
Shogi/BackEnd/Identity/ServerAccountManager.cs
Normal file
72
Shogi/BackEnd/Identity/ServerAccountManager.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace Shogi.BackEnd.Identity;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Shogi.FrontEnd.Client;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
public class ServerAccountManager(
|
||||
UserManager<ShogiUser> userManager,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IEmailSender emailSender) : IAccountManagement
|
||||
{
|
||||
public async Task<FormResult> RegisterAsync(string email, string password)
|
||||
{
|
||||
var user = new ShogiUser { UserName = email, Email = email };
|
||||
var result = await userManager.CreateAsync(user, password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var userId = await userManager.GetUserIdAsync(user);
|
||||
var code = await userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
var request = httpContextAccessor.HttpContext?.Request;
|
||||
var host = request?.Host.Value ?? "localhost";
|
||||
var scheme = request?.Scheme ?? "https";
|
||||
var callbackUrl = $"{scheme}://{host}/backend/confirmEmail?userId={userId}&code={code}";
|
||||
|
||||
await emailSender.SendEmailAsync(email, "Confirm your email",
|
||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
return new FormResult { Succeeded = true };
|
||||
}
|
||||
return new FormResult { Succeeded = false, ErrorList = result.Errors.Select(e => e.Description).ToArray() };
|
||||
}
|
||||
|
||||
public Task<FormResult> LoginAsync(string email, string password)
|
||||
{
|
||||
throw new NotSupportedException("Login must be performed via Form POST to /backend/Account/Login in Server-Side Rendering.");
|
||||
}
|
||||
|
||||
public Task LogoutAsync()
|
||||
{
|
||||
// Logout should be performed via Form POST or Link to /backend/Account/Logout.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> CheckAuthenticatedAsync()
|
||||
{
|
||||
return Task.FromResult(httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> RequestPasswordReset(string email)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user != null)
|
||||
{
|
||||
// Generate token and send email logic would go here.
|
||||
}
|
||||
return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
public async Task<FormResult> ChangePassword(string email, string resetCode, string newPassword)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user == null) return new FormResult { Succeeded = false, ErrorList = ["User not found"] };
|
||||
var result = await userManager.ResetPasswordAsync(user, resetCode, newPassword);
|
||||
return new FormResult { Succeeded = result.Succeeded, ErrorList = result.Errors.Select(e => e.Description).ToArray() };
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Shogi.Api.Identity;
|
||||
namespace Shogi.BackEnd.Identity;
|
||||
|
||||
public class ShogiUser : IdentityUser
|
||||
{
|
||||
@@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Shogi.Api.Identity;
|
||||
using Shogi.BackEnd.Identity;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Shogi.Api.Migrations
|
||||
namespace Shogi.BackEnd.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240816002834_InitialCreate")]
|
||||
@@ -158,7 +158,7 @@ namespace Shogi.Api.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Shogi.Api.Models.User", b =>
|
||||
modelBuilder.Entity("Shogi.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
@@ -234,7 +234,7 @@ namespace Shogi.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Shogi.Api.Models.User", null)
|
||||
b.HasOne("Shogi.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -243,7 +243,7 @@ namespace Shogi.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Shogi.Api.Models.User", null)
|
||||
b.HasOne("Shogi.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -258,7 +258,7 @@ namespace Shogi.Api.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Shogi.Api.Models.User", null)
|
||||
b.HasOne("Shogi.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -267,7 +267,7 @@ namespace Shogi.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Shogi.Api.Models.User", null)
|
||||
b.HasOne("Shogi.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Shogi.Api.Migrations
|
||||
namespace Shogi.BackEnd.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
@@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Shogi.Api.Identity;
|
||||
using Shogi.BackEnd.Identity;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Shogi.Api.Migrations
|
||||
namespace Shogi.BackEnd.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
@@ -155,7 +155,7 @@ namespace Shogi.Api.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Shogi.Api.Models.User", b =>
|
||||
modelBuilder.Entity("Shogi.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
@@ -231,7 +231,7 @@ namespace Shogi.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Shogi.Api.Models.User", null)
|
||||
b.HasOne("Shogi.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -240,7 +240,7 @@ namespace Shogi.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Shogi.Api.Models.User", null)
|
||||
b.HasOne("Shogi.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -255,7 +255,7 @@ namespace Shogi.Api.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Shogi.Api.Models.User", null)
|
||||
b.HasOne("Shogi.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -264,7 +264,7 @@ namespace Shogi.Api.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Shogi.Api.Models.User", null)
|
||||
b.HasOne("Shogi.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user