Files
BuYuHouDuan/ChessGameHost/Services/ChessBotService.cs
T
2026-10-12 16:22:04 +08:00

268 lines
9.9 KiB
C#

using ChessGameDomain;
using ChessGameService;
using CommonDomain;
using CommonDomain.BaseModel;
using CommonDomain.Enum;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MongoRepository;
using RedisHelper;
using RedisService;
using Common.Redis;
namespace ChessGameHost.Services;
public class ChessBotService : BackgroundService
{
private readonly ILogger<ChessBotService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IHubContext<ChessGameHub> _hubContext;
private static readonly Random _random = new();
public ChessBotService(
ILogger<ChessBotService> logger,
IServiceScopeFactory scopeFactory,
IHubContext<ChessGameHub> hubContext)
{
_logger = logger;
_scopeFactory = scopeFactory;
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
await CleanStaleRooms();
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope();
var redisService = scope.ServiceProvider.GetRequiredService<IRedisService>();
var distributedLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>();
var emptyRoomRateRepo = scope.ServiceProvider.GetRequiredService<IMongoBaseRepository<EmptyRoomRateConfig>>();
var rooms = redisService.GetAllChessRooms();
if (rooms == null) rooms = new List<ChessGameDomain.ChessGameRoom>();
if (rooms.Count > 0)
{
var robotRoomAllowance = await EnsureChessEmptyRoomRate(
emptyRoomRateRepo, redisService, distributedLock, rooms);
var candidateRooms = rooms.Where(r =>
r != null
&& r.state == 0
&& (r.users == null || r.users.Count == 0)
&& r.robotCount == 0
).ToList();
int maxBatch = Math.Min(Math.Min(candidateRooms.Count, robotRoomAllowance), 5);
for (int i = 0; i < maxBatch; i++)
{
var candidate = candidateRooms[i];
ChessGameRoom? updatedRoom = null;
await distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}chess:room:{candidate.RoomId}",
() =>
{
var target = redisService.GetChessRoom(candidate.RoomId);
if (target == null || target.state == 3 ||
(target.users != null && target.users.Count > 0))
return Task.CompletedTask;
target.robotCount = 1;
target.state = 1;
if (string.IsNullOrEmpty(target.password))
target.password = _random.Next(100000, 999999).ToString();
redisService.SetChessRoom(target);
updatedRoom = target;
return Task.CompletedTask;
},
expiry: 10);
if (updatedRoom != null)
await _hubContext.Clients.Group(ChessGameHub.LobbyGroup).SendAsync("JoinRoomNotice", updatedRoom);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "ChessBotService 执行异常");
}
await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken);
}
}
private async Task CleanStaleRooms()
{
try
{
using var scope = _scopeFactory.CreateScope();
var redisService = scope.ServiceProvider.GetRequiredService<IRedisService>();
var distributedLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>();
var rooms = redisService.GetAllChessRooms();
if (rooms == null) return;
var staleRooms = rooms.Where(r => r != null
&& (r.users == null || r.users.Count == 0)
&& (r.state == 3 || (r.state == 1 && string.IsNullOrWhiteSpace(r.createUserId))))
.ToList();
if (staleRooms.Count == 0) return;
foreach (var room in staleRooms)
{
await distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}chess:room:{room.RoomId}",
() =>
{
var target = redisService.GetChessRoom(room.RoomId);
if (target == null) return Task.CompletedTask;
if (target.state == 1 && string.IsNullOrWhiteSpace(target.createUserId)
&& (target.users == null || target.users.Count == 0))
{
redisService.RemoveChessRoom(target.RoomId);
return Task.CompletedTask;
}
target.users = new List<ChessGameDomain.RedisChessGameUser>();
target.state = 0;
target.password = "";
target.createUserId = "";
target.gold = 0;
target.gameType = 0;
target.robotCount = 0;
target.IsGameEnded = false;
target.EndGameRequests?.Clear();
target.PendingWinUserId = null;
target.TimeoutLoserUserId = "";
target.LastActiveTime = 0;
redisService.SetChessRoom(target);
return Task.CompletedTask;
},
expiry: 5);
await Task.Delay(50);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[ChessBot] 启动清理异常");
}
}
private async Task<int> EnsureChessEmptyRoomRate(
IMongoBaseRepository<EmptyRoomRateConfig> emptyRoomRateRepo,
IRedisService redisService,
IDistributedLock distributedLock,
List<ChessGameRoom> rooms)
{
try
{
var chessConfigs = emptyRoomRateRepo
.FilterBy(c => c.GameType == GameTypeEnum.中国象棋 && !c.IsDelete)
.ToList();
if (chessConfigs == null || chessConfigs.Count == 0)
return 0;
var threshold = chessConfigs.Max(c => c.EmptyRoomRate);
if (threshold <= 0)
return rooms.Count;
if (rooms == null || rooms.Count == 0)
return 0;
var total = rooms.Count(r => r != null);
var requiredEmpty = (int)Math.Ceiling(total * Math.Clamp(threshold, 0, 100) / 100.0);
var empty = rooms.Count(IsAvailableEmptyRoom);
var needRelease = Math.Max(0, requiredEmpty - empty);
if (needRelease > 0)
{
var robotOnlyRooms = rooms
.Where(r => r != null && !HasActiveHuman(r) && r.robotCount > 0)
.OrderBy(r => r.CreateTime)
.Take(needRelease)
.ToList();
foreach (var room in robotOnlyRooms)
{
ChessGameRoom? releasedRoom = null;
await distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}chess:room:{room.RoomId}",
() =>
{
var target = redisService.GetChessRoom(room.RoomId);
if (target == null || HasActiveHuman(target) || target.robotCount <= 0)
return Task.CompletedTask;
ResetToEmptyRoom(target);
redisService.SetChessRoom(target);
releasedRoom = target;
return Task.CompletedTask;
},
expiry: 10,
retry: true,
retryDelay: 50);
if (releasedRoom != null)
{
empty++;
await _hubContext.Clients.Group(ChessGameHub.LobbyGroup)
.SendAsync("ExitRoomNotice", releasedRoom);
}
}
}
return Math.Max(0, empty - requiredEmpty);
}
catch (Exception ex)
{
_logger.LogError(ex, "ChessBotService 维护空房率失败");
return 0;
}
}
private static bool IsAvailableEmptyRoom(ChessGameRoom room)
{
return room != null
&& !HasActiveHuman(room)
&& room.robotCount == 0
&& room.state != 2;
}
private static void ResetToEmptyRoom(ChessGameRoom room)
{
room.users = new List<RedisChessGameUser>();
room.state = 0;
room.password = "";
room.createUserId = "";
room.gold = 0;
room.gameType = 0;
room.robotCount = 0;
room.IsGameEnded = false;
room.EndGameRequests?.Clear();
room.PendingWinUserId = null;
room.TimeoutLoserUserId = "";
room.LastActiveTime = 0;
}
private static bool HasActiveHuman(ChessGameRoom room)
{
return room?.users != null && room.users.Any(u => u != null && !u.IsLeave);
}
}