feat(mm): wait longer for a real opponent; add "start with bots now"
- server: a lone player in the online-league queue now keeps waiting (re-checking every 15s) up to 75s so an online opponent has a real chance to join; the moment a 2nd human queues they're matched together, and a full 4 still forms instantly. Add PlayNow hub method to force-start with bots on demand. - client: matchmaking screen shows a "شروع با ربات / Start with bots" button after a few seconds so the player can skip the wait; waiting copy updated; raise the "connection stuck" hint threshold to 90s so it no longer fires during normal waits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,11 +17,14 @@ public sealed class Player
|
||||
/// <summary>In-memory matchmaking + room registry. (EF/Postgres persistence is a TODO.)</summary>
|
||||
public sealed partial class GameManager
|
||||
{
|
||||
// Real players get priority: wait exactly 15s for humans to join; whoever
|
||||
// hasn't joined the table by then is replaced with a bot when the match forms.
|
||||
// (Mirror of MATCH_QUEUE_WAIT_MS on the client — keep both in sync.)
|
||||
// Real players get priority. We re-check the queue every QueueWaitMs; the
|
||||
// moment a second human is waiting they're matched together (+ bots for any
|
||||
// empty seats), and a full group of 4 forms instantly. A player left ALONE
|
||||
// keeps waiting up to MaxAloneWaitMs so an online opponent has a genuine
|
||||
// chance to join before we fall back to a bot table. (QueueWaitMs mirrors
|
||||
// MATCH_QUEUE_WAIT_MS on the client — keep both in sync.)
|
||||
private const int QueueWaitMs = 15000;
|
||||
private int NextQueueWaitMs() => QueueWaitMs;
|
||||
private const int MaxAloneWaitMs = 75000;
|
||||
|
||||
private static readonly string[] BotNames =
|
||||
{ "آرش", "کیان", "نیلوفر", "سارا", "رضا", "مهسا", "امیر", "پارسا", "الناز", "بابک" };
|
||||
@@ -33,7 +36,7 @@ public sealed partial class GameManager
|
||||
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 List<(Player player, Timer timer, DateTime since)> _waiting = new();
|
||||
private readonly Random _rng = new();
|
||||
|
||||
public GameManager(IHubContext<GameHub> hub, IServiceScopeFactory scopes)
|
||||
@@ -60,8 +63,8 @@ public sealed partial class GameManager
|
||||
lock (_mmLock)
|
||||
{
|
||||
if (_waiting.Any(w => w.player.UserId == p.UserId)) return;
|
||||
var timer = new Timer(_ => FlushTicket(p.UserId), null, NextQueueWaitMs(), Timeout.Infinite);
|
||||
_waiting.Add((p, timer));
|
||||
var timer = new Timer(_ => FlushTicket(p.UserId), null, QueueWaitMs, Timeout.Infinite);
|
||||
_waiting.Add((p, timer, DateTime.UtcNow));
|
||||
_ = _hub.Clients.User(p.UserId).SendAsync("matchmaking",
|
||||
new MatchmakingStateDto("searching", _waiting.Count, null));
|
||||
if (_waiting.Count >= 4) FormGroupLocked(4);
|
||||
@@ -77,12 +80,37 @@ public sealed partial class GameManager
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushTicket(string userId)
|
||||
/// <summary>Player asked to start right now — match any humans waiting and
|
||||
/// fill the rest with bots, instead of waiting out the queue.</summary>
|
||||
public void PlayNow(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
|
||||
FormGroupLocked(_waiting.Count);
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushTicket(string userId)
|
||||
{
|
||||
lock (_mmLock)
|
||||
{
|
||||
var idx = _waiting.FindIndex(w => w.player.UserId == userId);
|
||||
if (idx < 0) return;
|
||||
|
||||
// A second human is already waiting → seat them together now and let
|
||||
// bots fill any empty chairs (real players matched immediately).
|
||||
if (_waiting.Count >= 2) { FormGroupLocked(_waiting.Count); return; }
|
||||
|
||||
// Alone: keep the table open for an online opponent until the max wait
|
||||
// elapses, then fall back to a bot table. Re-arm the re-check timer.
|
||||
var waited = (DateTime.UtcNow - _waiting[idx].since).TotalMilliseconds;
|
||||
if (waited < MaxAloneWaitMs)
|
||||
{
|
||||
_waiting[idx].timer.Change(QueueWaitMs, Timeout.Infinite);
|
||||
return;
|
||||
}
|
||||
FormGroupLocked(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ public sealed class GameHub : Hub
|
||||
});
|
||||
|
||||
public void CancelMatchmaking() => _manager.CancelMatchmaking(Uid);
|
||||
public void PlayNow() => _manager.PlayNow(Uid);
|
||||
public void PlayCard(string cardId) => _manager.PlayCard(Uid, cardId);
|
||||
public void ChooseTrump(string suit) => _manager.ChooseTrump(Uid, suit);
|
||||
public void SendReaction(string reaction) => _manager.SendReaction(Uid, reaction);
|
||||
|
||||
Reference in New Issue
Block a user