convert to blazor server side render

This commit is contained in:
2025-12-24 16:43:51 -06:00
parent 357c3d9932
commit dcbf8a3ac3
215 changed files with 1867 additions and 2350 deletions

View File

@@ -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(),
};
}
}

View File

@@ -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);
});
}

View File

@@ -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
}
}
}

View File

@@ -1,45 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.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>

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.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>

View File

@@ -1,11 +0,0 @@
using System.Text.Json;
namespace Shogi.Contracts;
public class ShogiApiJsonSerializerSettings
{
public readonly static JsonSerializerOptions SystemTextJsonSerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
};
}

View File

@@ -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; }
}
}

View File

@@ -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 a role and grant execute permission to that role
--CREATE ROLE db_executor --CREATE ROLE db_executor
--GRANT EXECUTE To 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. 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.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
*/ */

View File

@@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.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>

View File

@@ -1,15 +0,0 @@
namespace Shogi.Domain.ValueObjects;
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);
}

View File

@@ -1,6 +0,0 @@
namespace Shogi.Domain.ValueObjects
{
public record MoveResult(bool IsSuccess, string Reason = "")
{
}
}

View File

@@ -1,47 +0,0 @@
using Shogi.Domain.ValueObjects.Movement;
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;
}
}

View File

@@ -1,32 +0,0 @@
using Shogi.Domain.ValueObjects.Movement;
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(),
};
}
}

View File

@@ -1,31 +0,0 @@
using Shogi.Domain.ValueObjects.Movement;
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(),
};
}
}

View File

@@ -1,31 +0,0 @@
using Shogi.Domain.ValueObjects.Movement;
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(),
};
}
}

View File

@@ -1,99 +0,0 @@
using Shogi.Domain.ValueObjects.Movement;
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;
}
}
}

View File

@@ -1,35 +0,0 @@
using Shogi.Domain.ValueObjects.Movement;
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(),
};
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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}");
}
}
}

View File

@@ -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.

View File

@@ -1,12 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"microsoft.dotnet-msidentity": {
"version": "1.0.5",
"commands": [
"dotnet-msidentity"
]
}
}
}

View File

@@ -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);
}
}

View File

@@ -1 +0,0 @@
@using Shogi.UI.Pages.Home.VisualAids

View File

@@ -1,6 +0,0 @@
@using Contracts.Types;
<GameBoardPresentation Perspective="WhichPlayer.Player1" />
@code {
}

View File

@@ -1,64 +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.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 https://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);
}

View File

@@ -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;
}
}
}
}

View File

@@ -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>

View File

@@ -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
}
}
}

View File

@@ -1,8 +0,0 @@
{
"dependencies": {
"identityapp1": {
"type": "identityapp",
"dynamicId": null
}
}
}

View File

@@ -1,8 +0,0 @@
{
"dependencies": {
"identityapp1": {
"type": "identityapp.default",
"dynamicId": null
}
}
}

View File

@@ -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);
}

View File

@@ -1,63 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.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\OpponentName.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\OpponentName.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.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>

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -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/"
}

View File

@@ -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">
const 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>

View File

@@ -1,18 +1,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 18
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 18.3.11312.210
MinimumVisualStudioVersion = 10.0.40219.1 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E69DE334-29A7-46AE-9647-54DC0187CD8D}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
@@ -22,65 +12,52 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md README.md = README.md
EndProjectSection EndProjectSection
EndProject 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}" Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Shogi.Database", "Shogi.Database\Shogi.Database.sqlproj", "{9B115B71-088F-41EF-858F-C7B155271A9F}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Api", "Shogi.Api\Shogi.Api.csproj", "{62604006-6E18-45DA-8D5A-6ADD1C6D3CE2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "E2ETests", "Tests\E2ETests\E2ETests.csproj", "{401120C3-45D6-4A23-8D87-C2BED29F4950}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoardRules", "BoardRules\BoardRules.csproj", "{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoardRules", "BoardRules\BoardRules.csproj", "{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{20DA20BB-85F1-4DBE-9B22-3C4FAF89647B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shogi.AcceptanceTests", "Tests\AcceptanceTests\Shogi.AcceptanceTests.csproj", "{768F37ED-FB62-A57F-BCFA-91F26B4F794F}"
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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution 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.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.Build.0 = Debug|Any CPU
{9B115B71-088F-41EF-858F-C7B155271A9F}.Debug|Any CPU.Deploy.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.ActiveCfg = Release|Any CPU
{9B115B71-088F-41EF-858F-C7B155271A9F}.Release|Any CPU.Deploy.0 = 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.ActiveCfg = Debug|Any CPU
{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.Build.0 = Release|Any CPU {5B2F47A0-6AD5-4DA9-9CFE-9F52F634DD5E}.Release|Any CPU.Build.0 = Release|Any CPU
{768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{768F37ED-FB62-A57F-BCFA-91F26B4F794F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{768F37ED-FB62-A57F-BCFA-91F26B4F794F}.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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{4F93F735-DCCE-4A5D-ADDC-E0986DE4C48D} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320} {768F37ED-FB62-A57F-BCFA-91F26B4F794F} = {20DA20BB-85F1-4DBE-9B22-3C4FAF89647B}
{30F4E3DB-027F-4885-BE06-884167C1C6CF} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320} {9D1DD2CD-7B04-4472-4377-027563F356CA} = {20DA20BB-85F1-4DBE-9B22-3C4FAF89647B}
{401120C3-45D6-4A23-8D87-C2BED29F4950} = {A968C8E6-47B7-4F72-A27A-AC9B643FD320}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}

View File

@@ -1,4 +1,4 @@
namespace Shogi.Api; namespace Shogi;
public class ApiKeys public class ApiKeys
{ {

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
namespace Shogi.Api.Application; namespace Shogi.BackEnd.Application;
/// <summary> /// <summary>
/// Used to receive signals from connected clients. /// Used to receive signals from connected clients.

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
namespace Shogi.Api.Application; namespace Shogi.BackEnd.Application;
/// <summary> /// <summary>
/// Used to send signals to connected clients. /// Used to send signals to connected clients.

View File

@@ -1,15 +1,14 @@
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Shogi.Api.Controllers; using Shogi.BackEnd.Controllers;
using Shogi.Api.Extensions; using Shogi.BackEnd.Extensions;
using Shogi.Api.Identity; using Shogi.BackEnd.Identity;
using Shogi.Api.Repositories; using Shogi.BackEnd.Repositories;
using Shogi.Api.Repositories.Dto; using Shogi.BackEnd.Repositories.Dto;
using Shogi.Contracts.Api.Commands; using Shogi.BackEnd.Domains.Aggregates;
using Shogi.Domain.Aggregates;
using System.Data.SqlClient; using System.Data.SqlClient;
namespace Shogi.Api.Application; namespace Shogi.BackEnd.Application;
public class ShogiApplication( public class ShogiApplication(
QueryRepository queryRepository, QueryRepository queryRepository,
@@ -72,10 +71,10 @@ public class ShogiApplication(
return session; 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); var session = await this.ReadSession(sessionId);
if (session == null) if (session is null)
{ {
return new NotFoundResult(); return new NotFoundResult();
} }
@@ -105,7 +104,7 @@ public class ShogiApplication(
public async Task<IActionResult> JoinSession(string sessionId, string player2Id) public async Task<IActionResult> JoinSession(string sessionId, string player2Id)
{ {
var session = await this.ReadSession(sessionId); var session = await this.ReadSession(sessionId);
if (session == null) return new NotFoundResult(); if (session is null) return new NotFoundResult();
if (string.IsNullOrEmpty(session.Player2)) if (string.IsNullOrEmpty(session.Player2))
{ {

View File

@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Shogi.Api.Identity; using Shogi.BackEnd.Identity;
using System.Security.Claims; using System.Security.Claims;
namespace Shogi.Api.Controllers; namespace Shogi.BackEnd.Controllers;
[Authorize] [Authorize]
[Route("[controller]")] [Route("[controller]")]

View File

@@ -1,6 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
namespace Shogi.Api.Controllers; namespace Shogi.BackEnd.Controllers;
public static class Extentions public static class Extentions
{ {

View File

@@ -15,7 +15,7 @@ using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
namespace Shogi.Api.Controllers; namespace Shogi.BackEnd.Controllers;
/// <summary> /// <summary>
/// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add identity endpoints. /// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add identity endpoints.
@@ -407,7 +407,7 @@ public static class MyIdentityApiEndpointRouteBuilderExtensions
routeValues.Add("changedEmail", email); 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) var confirmEmailUrl = linkGenerator.GetUriByName(context, confirmEmailEndpointName, routeValues, host: host)
?? throw new NotSupportedException($"Could not find endpoint named '{confirmEmailEndpointName}'."); ?? throw new NotSupportedException($"Could not find endpoint named '{confirmEmailEndpointName}'.");

View File

@@ -1,12 +1,11 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Shogi.Api.Application; using Shogi.BackEnd.Application;
using Shogi.Api.Extensions; using Shogi.BackEnd.Extensions;
using Shogi.Api.Repositories; using Shogi.BackEnd.Repositories;
using Shogi.Contracts.Api.Commands; using Shogi.BackEnd.Types;
using Shogi.Contracts.Types;
namespace Shogi.Api.Controllers; namespace Shogi.BackEnd.Controllers;
[Authorize] [Authorize]
[ApiController] [ApiController]
@@ -57,25 +56,25 @@ public class SessionsController(
[AllowAnonymous] [AllowAnonymous]
public async Task<ActionResult<Session>> GetSession(Guid sessionId) public async Task<ActionResult<Session>> GetSession(Guid sessionId)
{ {
var session = await application.ReadSession(sessionId.ToString()); var domainSession = await application.ReadSession(sessionId.ToString());
if (session == null) return this.NotFound(); if (domainSession is null) return this.NotFound();
return new Session return new Session
{ {
BoardState = new BoardState BoardState = new BoardState
{ {
Board = session.Board.BoardState.State.ToContract(), Board = domainSession.Board.BoardState.State.ToContract(),
Player1Hand = session.Board.BoardState.Player1Hand.ToContract(), Player1Hand = domainSession.Board.BoardState.Player1Hand.ToContract(),
Player2Hand = session.Board.BoardState.Player2Hand.ToContract(), Player2Hand = domainSession.Board.BoardState.Player2Hand.ToContract(),
PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(), PlayerInCheck = domainSession.Board.BoardState.InCheck?.ToContract(),
WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract(), WhoseTurn = domainSession.Board.BoardState.WhoseTurn.ToContract(),
Victor = session.Board.BoardState.IsCheckmate Victor = domainSession.Board.BoardState.IsCheckmate
? session.Board.BoardState.InCheck == Domain.ValueObjects.WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1 ? domainSession.Board.BoardState.InCheck == Domains.ValueObjects.WhichPlayer.Player1 ? WhichPlayer.Player2 : WhichPlayer.Player1
: null : null
}, },
Player1 = application.GetUsername(session.Player1), Player1 = application.GetUsername(domainSession.Player1),
Player2 = application.GetUsername(session.Player2), Player2 = application.GetUsername(domainSession.Player2),
SessionId = session.Id SessionId = domainSession.Id
}; };
} }

View File

@@ -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) public class Session(Guid id, string player1Name)
{ {

View File

@@ -1,8 +1,11 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Shogi.Domain.YetToBeAssimilatedIntoDDD; using System.Numerics;
using BoardTile = System.Collections.Generic.KeyValuePair<System.Numerics.Vector2, Shogi.Domain.ValueObjects.Piece>; 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 public class BoardState
{ {

View File

@@ -1,4 +1,4 @@
namespace Shogi.Domain.ValueObjects; namespace Shogi.BackEnd.Domains.ValueObjects;
[Flags] [Flags]
internal enum InCheckResult internal enum InCheckResult

View 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);
}

View File

@@ -1,4 +1,4 @@
namespace Shogi.Domain.ValueObjects; namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
public enum Distance public enum Distance
{ {

View File

@@ -1,4 +1,6 @@
namespace Shogi.Domain.ValueObjects; using System.Numerics;
namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
/// <summary> /// <summary>
/// Represents a single piece being moved by a player from <paramref name="From"/> to <paramref name="To"/>. /// Represents a single piece being moved by a player from <paramref name="From"/> to <paramref name="To"/>.

View File

@@ -0,0 +1,5 @@
namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
public record MoveResult(bool IsSuccess, string Reason = "")
{
}

View File

@@ -1,6 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Numerics;
namespace Shogi.Domain.ValueObjects.Movement; namespace Shogi.BackEnd.Domains.ValueObjects.Movement;
[DebuggerDisplay("{Step} - {Distance}")] [DebuggerDisplay("{Step} - {Distance}")]
public record Path public record Path

View 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;
}

View File

@@ -1,7 +1,8 @@
using Shogi.Domain.ValueObjects.Movement; using Shogi.BackEnd.Domains.ValueObjects.Movement;
using System.Collections.ObjectModel; 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 internal record class GoldGeneral : Piece
{ {

View File

@@ -1,7 +1,8 @@
using Shogi.Domain.ValueObjects.Movement; using Shogi.BackEnd.Domains.ValueObjects.Movement;
using System.Collections.ObjectModel; 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 internal record class King : Piece
{ {

View 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(),
};
}

View 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(),
};
}

View 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(),
};
}

View 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;
}
}

View File

@@ -1,7 +1,8 @@
using Shogi.Domain.ValueObjects.Movement; using Shogi.BackEnd.Domains.ValueObjects.Movement;
using System.Collections.ObjectModel; 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 public record class Rook : Piece
{ {

View 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(),
};
}

View File

@@ -1,6 +1,10 @@
using Shogi.Domain.ValueObjects.Movement; using System.Numerics;
using Shogi.Domain.YetToBeAssimilatedIntoDDD; using Shogi.BackEnd.Domains.ValueObjects.Movement;
namespace Shogi.Domain.ValueObjects; 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> /// <summary>
/// Facilitates Shogi board state transitions, cognisant of Shogi rules. /// Facilitates Shogi board state transitions, cognisant of Shogi rules.

View 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);
}
}

View File

@@ -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);
}
}

View 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}");
}
}

View 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(),
};
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Shogi.Api.Identity; namespace Shogi.BackEnd.Identity;
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ShogiUser>(options) public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ShogiUser>(options)
{ {

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace Shogi.Api.Identity; namespace Shogi.BackEnd.Identity;
public class ShogiUser : IdentityUser public class ShogiUser : IdentityUser
{ {

View File

@@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Shogi.Api.Identity; using Shogi.BackEnd.Identity;
#nullable disable #nullable disable
namespace Shogi.Api.Migrations namespace Shogi.BackEnd.Migrations
{ {
[DbContext(typeof(ApplicationDbContext))] [DbContext(typeof(ApplicationDbContext))]
[Migration("20240816002834_InitialCreate")] [Migration("20240816002834_InitialCreate")]
@@ -158,7 +158,7 @@ namespace Shogi.Api.Migrations
b.ToTable("AspNetUserTokens", (string)null); b.ToTable("AspNetUserTokens", (string)null);
}); });
modelBuilder.Entity("Shogi.Api.Models.User", b => modelBuilder.Entity("Shogi.Models.User", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
@@ -234,7 +234,7 @@ namespace Shogi.Api.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{ {
b.HasOne("Shogi.Api.Models.User", null) b.HasOne("Shogi.Models.User", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -243,7 +243,7 @@ namespace Shogi.Api.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{ {
b.HasOne("Shogi.Api.Models.User", null) b.HasOne("Shogi.Models.User", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -258,7 +258,7 @@ namespace Shogi.Api.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Shogi.Api.Models.User", null) b.HasOne("Shogi.Models.User", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -267,7 +267,7 @@ namespace Shogi.Api.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{ {
b.HasOne("Shogi.Api.Models.User", null) b.HasOne("Shogi.Models.User", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)

View File

@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
namespace Shogi.Api.Migrations namespace Shogi.BackEnd.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class InitialCreate : Migration public partial class InitialCreate : Migration

View File

@@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Shogi.Api.Identity; using Shogi.BackEnd.Identity;
#nullable disable #nullable disable
namespace Shogi.Api.Migrations namespace Shogi.BackEnd.Migrations
{ {
[DbContext(typeof(ApplicationDbContext))] [DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot partial class ApplicationDbContextModelSnapshot : ModelSnapshot
@@ -155,7 +155,7 @@ namespace Shogi.Api.Migrations
b.ToTable("AspNetUserTokens", (string)null); b.ToTable("AspNetUserTokens", (string)null);
}); });
modelBuilder.Entity("Shogi.Api.Models.User", b => modelBuilder.Entity("Shogi.Models.User", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
@@ -231,7 +231,7 @@ namespace Shogi.Api.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{ {
b.HasOne("Shogi.Api.Models.User", null) b.HasOne("Shogi.Models.User", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -240,7 +240,7 @@ namespace Shogi.Api.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{ {
b.HasOne("Shogi.Api.Models.User", null) b.HasOne("Shogi.Models.User", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -255,7 +255,7 @@ namespace Shogi.Api.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Shogi.Api.Models.User", null) b.HasOne("Shogi.Models.User", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -264,7 +264,7 @@ namespace Shogi.Api.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{ {
b.HasOne("Shogi.Api.Models.User", null) b.HasOne("Shogi.Models.User", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)

View File

@@ -1,6 +1,6 @@
using Shogi.Domain.ValueObjects; using Shogi.BackEnd.Domains.ValueObjects;
namespace Shogi.Api.Repositories.Dto; namespace Shogi.BackEnd.Repositories.Dto;
/// <summary> /// <summary>
/// Useful with Dapper to read from database. /// Useful with Dapper to read from database.

View File

@@ -1,4 +1,4 @@
namespace Shogi.Api.Repositories.Dto; namespace Shogi.BackEnd.Repositories.Dto;
public readonly record struct SessionDto(string Id, string Player1Id, string Player2Id) public readonly record struct SessionDto(string Id, string Player1Id, string Player2Id)
{ {

View File

@@ -3,7 +3,7 @@ using Microsoft.Extensions.Options;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json; using System.Text.Json;
namespace Shogi.Api.Repositories; namespace Shogi.BackEnd.Repositories;
// https://app-smtp.brevo.com/real-time // https://app-smtp.brevo.com/real-time

View File

@@ -1,9 +1,9 @@
using Dapper; using Dapper;
using Shogi.Api.Repositories.Dto; using Shogi.BackEnd.Repositories.Dto;
using System.Data; using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
namespace Shogi.Api.Repositories; namespace Shogi.BackEnd.Repositories;
public class QueryRepository(IConfiguration configuration) public class QueryRepository(IConfiguration configuration)
{ {

View File

@@ -1,11 +1,10 @@
using Dapper; using Dapper;
using Shogi.Api.Repositories.Dto; using Shogi.BackEnd.Repositories.Dto;
using Shogi.Contracts.Api.Commands; using Shogi.BackEnd.Domains.Aggregates;
using Shogi.Domain.Aggregates;
using System.Data; using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
namespace Shogi.Api.Repositories; namespace Shogi.BackEnd.Repositories;
public class SessionRepository(IConfiguration configuration) public class SessionRepository(IConfiguration configuration)
{ {
@@ -53,7 +52,7 @@ public class SessionRepository(IConfiguration configuration)
return new(sessionDtos.First(), moveDtos); return new(sessionDtos.First(), moveDtos);
} }
public async Task CreateMove(string sessionId, MovePieceCommand command) public async Task CreateMove(string sessionId, Types.MovePieceCommand command)
{ {
using var connection = new SqlConnection(this.connectionString); using var connection = new SqlConnection(this.connectionString);
await connection.ExecuteAsync( await connection.ExecuteAsync(

View File

@@ -1,7 +1,4 @@
using System; namespace Shogi.BackEnd.Types;
using System.Collections.Generic;
namespace Shogi.Contracts.Types;
public class BoardState public class BoardState
{ {

View File

@@ -1,9 +1,7 @@
using Shogi.Contracts.Types;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Shogi.Contracts.Api.Commands; namespace Shogi.BackEnd.Types;
public partial class MovePieceCommand : IValidatableObject public partial class MovePieceCommand : IValidatableObject
{ {

View File

@@ -0,0 +1,8 @@
namespace Shogi.BackEnd.Types;
public class Piece
{
public bool IsPromoted { get; set; }
public WhichPiece WhichPiece { get; set; }
public WhichPlayer Owner { get; set; }
}

View File

@@ -1,6 +1,4 @@
using System; namespace Shogi.BackEnd.Types;
namespace Shogi.Contracts.Types;
public class Session public class Session
{ {
@@ -16,5 +14,5 @@ public class Session
public Guid SessionId { get; set; } public Guid SessionId { get; set; }
public BoardState BoardState { get; set; } public BoardState BoardState { get; set; } = new();
} }

View File

@@ -1,6 +1,4 @@
using System; namespace Shogi.BackEnd.Types;
namespace Shogi.Contracts.Types;
public class SessionMetadata public class SessionMetadata
{ {

View File

@@ -1,4 +1,4 @@
namespace Shogi.Contracts.Types; namespace Shogi.BackEnd.Types;
public enum WhichPiece public enum WhichPiece
{ {

View File

@@ -1,4 +1,4 @@
namespace Shogi.Contracts.Types; namespace Shogi.BackEnd.Types;
public enum WhichPlayer public enum WhichPlayer
{ {

View File

@@ -1,4 +1,4 @@
namespace Shogi.UI.Identity; namespace Shogi.FrontEnd.Client;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http; using System.Net.Http;

View File

@@ -1,4 +1,4 @@
namespace Shogi.UI.Identity; namespace Shogi.FrontEnd.Client;
public class FormResult public class FormResult
{ {

View File

@@ -1,28 +1,19 @@
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.Components;
using Shogi.UI.Identity; using Microsoft.AspNetCore.SignalR.Client;
namespace Shogi.UI.Shared; namespace Shogi.FrontEnd.Client;
public class GameHubNode : IAsyncDisposable public class GameHubNode : IAsyncDisposable
{ {
private readonly HubConnection hubConnection; private readonly HubConnection hubConnection;
public GameHubNode(IConfiguration configuration) public GameHubNode(NavigationManager navigationManager)
{ {
var baseUrl = configuration["ShogiApiUrl"]; var hubUrl = navigationManager.ToAbsoluteUri("/gamehub");
if (string.IsNullOrWhiteSpace(baseUrl))
{
throw new InvalidOperationException("ShogiApiUrl configuration is missing.");
}
this.hubConnection = new HubConnectionBuilder() this.hubConnection = new HubConnectionBuilder()
.WithUrl(new Uri(new Uri(baseUrl, UriKind.Absolute), "gamehub"), options => .WithUrl(hubUrl)
{ .Build();
options.HttpMessageHandlerFactory = handler => new CookieCredentialsMessageHandler { InnerHandler = handler };
options.SkipNegotiation = true;
options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
})
.Build();
this.hubConnection.Closed += this.HubConnection_Closed; this.hubConnection.Closed += this.HubConnection_Closed;
} }

View File

@@ -1,4 +1,4 @@
namespace Shogi.UI.Identity; namespace Shogi.FrontEnd.Client;
/// <summary> /// <summary>
/// Account management services. /// Account management services.

Some files were not shown because too many files have changed in this diff Show More