Server-authoritative economy: wire client to server; entry + rewards on hub

Server:
- daily (/api/daily, /api/daily/claim) + shop (/api/shop/buy) + ChargeEntry
- GameRoom (via IServiceScopeFactory) deducts ranked entry at match start and
  applies match rewards at match-over, broadcasting profile + reward over the hub
- tested: daily, shop (owned-guard), ranked entry deduction pushed over hub

Client:
- SignalrService routes profile/coins/plan/daily/shop/match to the server (Bearer);
  onProfile/onReward hub events; guest/offline fall back to local
- session-store syncs profile from hub; game-store serverReward; GameScreen shows
  live ranked reward from hub (no double submit), submits client-run games
- single source of truth in live mode (no economy divergence)

Postgres-ready via config (Provider=postgres); EnsureCreated for now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 17:32:47 +03:30
parent d0b8976713
commit 4f2e4e14ea
12 changed files with 341 additions and 31 deletions
+8 -2
View File
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using Hokm.Server.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
namespace Hokm.Server.Game;
@@ -24,13 +25,18 @@ public sealed class GameManager
{ "a-fox", "a-lion", "a-owl", "a-tiger", "a-panda", "a-eagle", "a-wolf", "a-cat" };
private readonly IHubContext<GameHub> _hub;
private readonly IServiceScopeFactory _scopes;
private readonly ConcurrentDictionary<string, GameRoom> _rooms = new();
private readonly ConcurrentDictionary<string, string> _userRoom = new(); // userId -> roomId
private readonly object _mmLock = new();
private readonly List<(Player player, Timer timer)> _waiting = new();
private readonly Random _rng = new();
public GameManager(IHubContext<GameHub> hub) => _hub = hub;
public GameManager(IHubContext<GameHub> hub, IServiceScopeFactory scopes)
{
_hub = hub;
_scopes = scopes;
}
/* ----------------------------- matchmaking ------------------------- */
@@ -105,7 +111,7 @@ public sealed class GameManager
}
}
var room = new GameRoom(_hub, seats, ranked: true, stake: 100, targetScore: 7);
var room = new GameRoom(_hub, _scopes, seats, ranked: true, stake: 100, targetScore: 7);
room.OnFinished = FinishRoom;
_rooms[room.Id] = room;
foreach (var h in humans) _userRoom[h.UserId] = room.Id;