Phase G: scaffold .NET 10 + SignalR backend (engine port + hub + auth)

- server/ monorepo: Hokm.Engine (C# port of TS engine+AI, validated by sim),
  Hokm.Server (SignalR GameHub, in-memory matchmaking/rooms, server-side turn
  timers + bot fill + disconnect handling, per-seat state broadcast), Hokm.Sim
- JWT dev auth (OTP 1234 + email); CORS for the Next client; /hub/game
- NuGet restored from mirrors (Soroush Nexus + Liara); NuGetAudit off
- README + .NET .gitignore; static class Engine renamed Rules (namespace clash)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 12:42:15 +03:30
parent ae239f4c51
commit aaf66b921f
22 changed files with 1220 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\src\Hokm.Engine\Hokm.Engine.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+52
View File
@@ -0,0 +1,52 @@
using Hokm.Engine;
// All-AI simulation to validate the C# engine port (mirrors scripts/sim.ts).
var rng = new Random(12345);
int N = 300;
int totalRounds = 0;
for (int i = 0; i < N; i++)
{
var (rounds, tricks) = PlayMatch(rng);
totalRounds += rounds;
if (tricks <= 0) throw new Exception("no tricks played");
}
Console.WriteLine($"OK: {N} matches completed. avg rounds/match = {(double)totalRounds / N:0.0}");
static (int rounds, int tricks) PlayMatch(Random rng)
{
var g = Rules.CreateInitial(new[] { "P0", "P1", "P2", "P3" }, 7);
Rules.SelectHakem(g, rng);
Rules.DealForTrump(g, rng);
int rounds = 0, tricks = 0, guard = 0;
while (g.Phase != Phase.MatchOver)
{
if (++guard > 200000) throw new Exception("loop guard tripped");
switch (g.Phase)
{
case Phase.ChoosingTrump:
Rules.ChooseTrump(g, Ai.ChooseTrump(g.Players[g.Hakem!.Value].Hand));
break;
case Phase.Playing:
int seat = g.Turn!.Value;
Rules.PlayCard(g, seat, Ai.ChooseCard(g, seat));
break;
case Phase.TrickComplete:
tricks++;
if (g.CurrentTrick.Count != 4) throw new Exception("trick not full");
Rules.AdvanceAfterTrick(g, 2);
break;
case Phase.RoundOver:
rounds++;
if (g.RoundTricks[0] + g.RoundTricks[1] > 13) throw new Exception("too many tricks");
Rules.StartNextRound(g, rng);
break;
default:
throw new Exception("unexpected phase: " + g.Phase);
}
}
return (rounds, tricks);
}