using System.Collections.Concurrent; using Hokm.Server.Hubs; using Microsoft.AspNetCore.SignalR; namespace Hokm.Server.Game; public sealed class Player { public required string UserId { get; init; } public string Name { get; init; } = ""; public string Avatar { get; init; } = "a-fox"; public int Level { get; init; } public string Plan { get; init; } = "free"; } /// In-memory matchmaking + room registry. (EF/Postgres persistence is a TODO.) public sealed class GameManager { private const int QueueWaitMs = 6000; private static readonly string[] BotNames = { "آرش", "کیان", "نیلوفر", "سارا", "رضا", "مهسا", "امیر", "پارسا", "الناز", "بابک" }; private static readonly string[] Avatars = { "a-fox", "a-lion", "a-owl", "a-tiger", "a-panda", "a-eagle", "a-wolf", "a-cat" }; private readonly IHubContext _hub; private readonly ConcurrentDictionary _rooms = new(); private readonly ConcurrentDictionary _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 hub) => _hub = hub; /* ----------------------------- matchmaking ------------------------- */ public void StartMatchmaking(Player p) { // Pro players skip the queue entirely. if (p.Plan == "pro") { StartMatch(new List { p }); return; } lock (_mmLock) { if (_waiting.Any(w => w.player.UserId == p.UserId)) return; var timer = new Timer(_ => FlushTicket(p.UserId), null, QueueWaitMs, Timeout.Infinite); _waiting.Add((p, timer)); _ = _hub.Clients.User(p.UserId).SendAsync("matchmaking", new MatchmakingStateDto("searching", _waiting.Count, null)); if (_waiting.Count >= 4) FormGroupLocked(4); } } public void CancelMatchmaking(string userId) { lock (_mmLock) { var idx = _waiting.FindIndex(w => w.player.UserId == userId); if (idx >= 0) { _waiting[idx].timer.Dispose(); _waiting.RemoveAt(idx); } } } private void FlushTicket(string userId) { lock (_mmLock) { if (!_waiting.Any(w => w.player.UserId == userId)) return; FormGroupLocked(_waiting.Count); // start with whoever is waiting; bots fill the rest } } private void FormGroupLocked(int count) { var take = _waiting.Take(Math.Min(count, 4)).ToList(); foreach (var w in take) w.timer.Dispose(); _waiting.RemoveAll(w => take.Any(t => t.player.UserId == w.player.UserId)); StartMatch(take.Select(t => t.player).ToList()); } /* ------------------------------- rooms ----------------------------- */ private void StartMatch(List humans) { var seats = new SeatSlot[4]; for (int i = 0; i < 4; i++) { if (i < humans.Count) { var h = humans[i]; seats[i] = new SeatSlot { Seat = i, UserId = h.UserId, Name = h.Name, Avatar = h.Avatar, Level = h.Level }; } else { seats[i] = new SeatSlot { Seat = i, IsBot = true, Name = BotNames[_rng.Next(BotNames.Length)], Avatar = Avatars[_rng.Next(Avatars.Length)], Level = _rng.Next(1, 50), }; } } var room = new GameRoom(_hub, seats, ranked: true, stake: 100, targetScore: 7); room.OnFinished = FinishRoom; _rooms[room.Id] = room; foreach (var h in humans) _userRoom[h.UserId] = room.Id; foreach (var h in humans) _ = _hub.Clients.User(h.UserId).SendAsync("matchFound", new { roomId = room.Id, seat = room.SeatOf(h.UserId) }); room.Start(); } private void FinishRoom(GameRoom room) { foreach (var uid in room.UserIds) _userRoom.TryRemove(uid, out _); // keep briefly for any late reads, then dispose _ = Task.Delay(15000).ContinueWith(_ => { if (_rooms.TryRemove(room.Id, out var r)) r.Dispose(); }); } private GameRoom? RoomOf(string userId) => _userRoom.TryGetValue(userId, out var id) && _rooms.TryGetValue(id, out var r) ? r : null; /* --------------------------- player actions ------------------------ */ public void PlayCard(string userId, string cardId) => RoomOf(userId)?.HumanPlay(userId, cardId); public void ChooseTrump(string userId, string suit) => RoomOf(userId)?.HumanChooseTrump(userId, suit); public void SendReaction(string userId, string reaction) => RoomOf(userId)?.Reaction(userId, reaction); private int _online; public int OnlineCount => Volatile.Read(ref _online); public void OnConnected(string userId) { Interlocked.Increment(ref _online); RoomOf(userId)?.SetConnected(userId, true); } public void OnDisconnected(string userId) { if (Interlocked.Decrement(ref _online) < 0) Interlocked.Exchange(ref _online, 0); CancelMatchmaking(userId); RoomOf(userId)?.SetConnected(userId, false); } }