namespace GeoSus.Server; using Microsoft.Extensions.Logging; using System.Threading.Channels; using System.Security.Cryptography; // Herní logika - kill, meeting, voting, tasks public class GameLogic { private readonly ServerConfig _config; private readonly ILogger _logger; private readonly OverpassService? _overpassService; private readonly Random _random = new Random(); // Sdílená instance pro lepší náhodnost public GameLogic(ServerConfig config, ILogger logger, OverpassService? overpassService = null) { _config = config; _logger = logger; _overpassService = overpassService; } #region Zabíjení public (bool Success, string? Error, Body? Body) TryKill( Player killer, Player victim, Dictionary players, List bodies) { // Validace - killer musí být impostor a alive if (killer.Role != PlayerRole.Impostor) return (false, "Nejsi impostor", null); if (killer.State != PlayerState.Alive) return (false, "Jsi mrtvý", null); if (victim.State != PlayerState.Alive) return (false, "Oběť je již mrtvá", null); // Cooldown kontrola if (killer.LastKillTime.HasValue) { var elapsed = (DateTime.UtcNow - killer.LastKillTime.Value).TotalMilliseconds; if (elapsed < _config.KillCooldownMs) { var remaining = (_config.KillCooldownMs - elapsed) / 1000; return (false, $"Cooldown: {remaining:F1}s", null); } } // Vzdálenost var distance = killer.Position.DistanceTo(victim.Position); if (distance > _config.KillDistanceM) return (false, $"Příliš daleko ({distance:F1}m)", null); // Kill úspěšný victim.State = PlayerState.Dead; killer.LastKillTime = DateTime.UtcNow; var body = new Body { BodyId = Guid.NewGuid().ToString("N")[..8], VictimId = victim.ClientUuid, KillerId = killer.ClientUuid, Location = victim.Position, KilledAt = DateTime.UtcNow }; bodies.Add(body); _logger.LogInformation("Kill: {Killer} zabil {Victim}, body {BodyId}", killer.ClientUuid, victim.ClientUuid, body.BodyId); return (true, null, body); } #endregion #region Reporty a meetingy public (bool Success, string? Error, Body? Body) TryReportBody( Player reporter, string bodyId, List bodies) { if (reporter.State != PlayerState.Alive) return (false, "Jsi mrtvý", null); var body = bodies.FirstOrDefault(b => b.BodyId == bodyId); if (body == null) return (false, "Tělo neexistuje", null); if (body.Reported) return (false, "Tělo již bylo reportnuto", null); // Vzdálenost var distance = reporter.Position.DistanceTo(body.Location); if (distance > _config.ReportDistanceM) return (false, $"Příliš daleko ({distance:F1}m)", null); body.Reported = true; body.ReportedBy = reporter.ClientUuid; _logger.LogInformation("Report: {Reporter} reportnul tělo {BodyId}", reporter.ClientUuid, bodyId); return (true, null, body); } public (bool Success, string? Error) TryCallEmergencyMeeting( Player caller, int maxMeetings, int cooldownMs) { if (caller.State != PlayerState.Alive) return (false, "Jsi mrtvý"); // Limit na počet emergency meetings if (caller.EmergencyMeetingsUsed >= maxMeetings) return (false, $"Vyčerpal jsi emergency meetingy ({maxMeetings})"); // Cooldown if (caller.LastEmergencyMeetingTime.HasValue) { var elapsed = (DateTime.UtcNow - caller.LastEmergencyMeetingTime.Value).TotalMilliseconds; if (elapsed < cooldownMs) { var remaining = (cooldownMs - elapsed) / 1000; return (false, $"Cooldown: {remaining:F1}s"); } } caller.EmergencyMeetingsUsed++; caller.LastEmergencyMeetingTime = DateTime.UtcNow; _logger.LogInformation("Emergency meeting: {Caller}", caller.ClientUuid); return (true, null); } public Meeting StartMeeting( MeetingType type, string callerId, Position meetingLocation, string? reportedBodyId, int arrivalBaseMs, int votingPhaseMs, int discussionPhaseMs) { var now = DateTime.UtcNow; return new Meeting { MeetingId = Guid.NewGuid().ToString("N")[..8], Type = type, CallerId = callerId, ReportedBodyId = reportedBodyId, MeetingLocation = meetingLocation, StartTime = now, ArrivalDeadline = now.AddMilliseconds(arrivalBaseMs + _config.ArrivalSafetyMarginMs), DiscussionEndTime = discussionPhaseMs > 0 ? now.AddMilliseconds(arrivalBaseMs + discussionPhaseMs) : null, VotingEndTime = now.AddMilliseconds(arrivalBaseMs + discussionPhaseMs + votingPhaseMs) }; } public bool CheckPlayerArrival(Player player, Meeting meeting) { if (player.State != PlayerState.Alive) return false; var distance = player.Position.DistanceTo(meeting.MeetingLocation); var now = DateTime.UtcNow; // Grace period var deadline = meeting.ArrivalDeadline.AddMilliseconds(_config.AllowedLateMs); if (now <= deadline && distance <= _config.MeetingArrivalRadiusM) { meeting.ArrivedPlayers.Add(player.ClientUuid); return true; } return false; } #endregion #region Hlasování public (bool Success, string? Error) TryCastVote( Player voter, string? targetId, Meeting meeting, Dictionary players) { if (voter.State != PlayerState.Alive) return (false, "Jsi mrtvý"); if (!meeting.ArrivedPlayers.Contains(voter.ClientUuid)) return (false, "Nedorazil jsi včas na meeting"); var now = DateTime.UtcNow; // Kontrola fáze - musíme být ve voting fázi if (meeting.DiscussionEndTime.HasValue && now < meeting.DiscussionEndTime.Value) return (false, "Ještě probíhá diskuze"); if (now > meeting.VotingEndTime) return (false, "Hlasování skončilo"); // Rate limit na změny hlasu if (meeting.Votes.ContainsKey(voter.ClientUuid) && meeting.LastVoteChangeTime.HasValue) { var sinceLastChange = (now - meeting.LastVoteChangeTime.Value).TotalMilliseconds; if (sinceLastChange < 2000) return (false, "Příliš rychlá změna hlasu"); } // Validace targetu if (targetId != null) { if (!players.TryGetValue(targetId, out var target)) return (false, "Neplatný cíl hlasu"); if (target.State != PlayerState.Alive) return (false, "Cíl je mrtvý"); } meeting.Votes[voter.ClientUuid] = targetId; meeting.LastVoteChangeTime = now; return (true, null); } public (string? EjectedId, bool WasTie, Dictionary VoteCounts) ResolveVoting( Meeting meeting, Dictionary players, TiePolicy tiePolicy) { var voteCounts = new Dictionary(); var skipCount = 0; foreach (var (voterId, targetId) in meeting.Votes) { if (targetId == null) { skipCount++; } else { voteCounts.TryAdd(targetId, 0); voteCounts[targetId]++; } } voteCounts["__SKIP__"] = skipCount; // Najdeme maximum var maxVotes = voteCounts.Values.Max(); var topVoted = voteCounts.Where(kv => kv.Value == maxVotes).Select(kv => kv.Key).ToList(); // Kontrola remízy if (topVoted.Count > 1 || topVoted.Contains("__SKIP__")) { // Remíza nebo skip vyhrál switch (tiePolicy) { case TiePolicy.NoEject: return (null, topVoted.Count > 1, voteCounts); case TiePolicy.Random: if (topVoted.Contains("__SKIP__")) topVoted.Remove("__SKIP__"); if (topVoted.Count > 0) { var random = new Random(); var ejected = topVoted[random.Next(topVoted.Count)]; return (ejected, true, voteCounts); } return (null, true, voteCounts); case TiePolicy.EjectLowestId: if (topVoted.Contains("__SKIP__")) topVoted.Remove("__SKIP__"); if (topVoted.Count > 0) { var ejected = topVoted.OrderBy(id => id).First(); return (ejected, true, voteCounts); } return (null, true, voteCounts); default: return (null, topVoted.Count > 1, voteCounts); } } var winner = topVoted[0]; if (winner == "__SKIP__") return (null, false, voteCounts); return (winner, false, voteCounts); } public void EjectPlayer(Player player) { player.State = PlayerState.Dead; _logger.LogInformation("Ejected: {Player}", player.ClientUuid); } #endregion #region Tasky /// /// Pokusí se dokončit task. Kontroluje: /// - Hráč je naživu /// - Není impostor /// - Task existuje a patří hráči /// - Task ještě není dokončen /// - Hráč je dostatečně blízko /// Poznámka: Duchové (mrtví hráči) MOHOU plnit úkoly - to je důležité pro crew! /// public (bool Success, string? Error) TryCompleteTask(Player player, string taskId) { // Duchové mohou plnit úkoly - neblokujeme mrtvé hráče // Ale musí být crew role if (player.Role == PlayerRole.Impostor) return (false, "Impostoři nemohou dělat tasky"); var task = player.Tasks.FirstOrDefault(t => t.TaskId == taskId); if (task == null) return (false, "Tento task ti nepatří"); if (player.CompletedTaskIds.Contains(taskId)) return (false, "Task již dokončen"); // Kontrola vzdálenosti var distance = player.Position.DistanceTo(task.Location); if (distance > _config.TaskStartDistanceM) return (false, $"Příliš daleko ({distance:F1}m, max {_config.TaskStartDistanceM}m)"); // Dokončit task player.CompletedTaskIds.Add(taskId); _logger.LogInformation("Task completed: {Player} dokončil {Task} na pozici {Pos} (vzdálenost {Dist:F1}m)", player.ClientUuid, task.Name, task.Location, distance); return (true, null); } /// /// Najde nejbližší nedokončený task hráče /// public GameTask? FindNearestTask(Player player, double maxDistance) { return player.Tasks .Where(t => !player.CompletedTaskIds.Contains(t.TaskId)) .Where(t => player.Position.DistanceTo(t.Location) <= maxDistance) .OrderBy(t => player.Position.DistanceTo(t.Location)) .FirstOrDefault(); } #endregion #region Win conditions public (bool GameOver, string? WinningFaction, string? Reason) CheckWinConditions( Dictionary players, List tasks) { var aliveCrew = players.Values.Count(p => p.State == PlayerState.Alive && p.Role == PlayerRole.Crew); var aliveImpostors = players.Values.Count(p => p.State == PlayerState.Alive && p.Role == PlayerRole.Impostor); // Všichni impostoři mrtví -> crew vyhrál (kontrolujeme nejdřív, ošetří // i edge case 0 impostorů + 0 crew - vrátí "crew vyhrál" místo aby // padlo dál do impostor-win větve) if (aliveImpostors == 0) { return (true, "Crew", "Všichni impostoři eliminováni"); } // Žádní crewmati naživu -> impostoři vyhráli (kritický fix: P9 - // při ejekci posledního crewmate stará podmínka `aliveCrew > 0` // shodila tuto větev a hra pokračovala s 0 crew naživu). if (aliveCrew == 0) { return (true, "Impostor", "Všichni crewmati eliminováni"); } // Impostoři vyhráli - mají většinu nebo rovnost (oba > 0) if (aliveImpostors >= aliveCrew) { return (true, "Impostor", "Impostoři mají převahu"); } // Všechny tasky hotové (počítáme pouze crew tasky) var crewPlayers = players.Values.Where(p => p.Role == PlayerRole.Crew).ToList(); var totalTasks = crewPlayers.Sum(p => p.Tasks.Count); var completedTasks = crewPlayers.Sum(p => p.CompletedTaskIds.Count); if (totalTasks > 0 && completedTasks >= totalTasks) { return (true, "Crew", "Všechny tasky dokončeny"); } return (false, null, null); } #endregion #region Role assignment public void AssignRoles(List players, int impostorCount) { if (players.Count < 2) { // Minimálně 2 hráči foreach (var p in players) p.Role = PlayerRole.Crew; return; } // Omezení počtu impostorů var maxImpostors = Math.Max(1, players.Count / 3); impostorCount = Math.Min(impostorCount, maxImpostors); impostorCount = Math.Min(impostorCount, players.Count - 1); // Náhodně vybereme impostory var shuffled = players.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue)).ToList(); for (int i = 0; i < players.Count; i++) { shuffled[i].Role = i < impostorCount ? PlayerRole.Impostor : PlayerRole.Crew; shuffled[i].State = PlayerState.Alive; shuffled[i].EmergencyMeetingsUsed = 0; shuffled[i].LastEmergencyMeetingTime = null; shuffled[i].LastKillTime = null; shuffled[i].CompletedTaskIds.Clear(); shuffled[i].CurrentTaskId = null; } _logger.LogInformation("Roles assigned: {ImpostorCount} impostorů z {TotalPlayers} hráčů", impostorCount, players.Count); } public List GenerateTasks(int count, Position center, double radius, MapData? mapData = null, int startIndex = 0) { var tasks = new List(); var taskNames = new[] { "Opravit kabel", "Zkalibrovat senzor", "Stáhnout data", "Nabít baterii", "Vyčistit filtr", "Nastavit kompas", "Opravit antenu", "Zkontrolovat zásoby", "Otestovat reaktor" }; // Get task positions - use map data if available List positions; if (mapData != null && _overpassService != null && mapData.ReachablePositions.Count >= count) { positions = _overpassService.GetPOIBasedPositions(mapData, count, _random); _logger.LogInformation("Generated {Count} positions for tasks using map data", positions.Count); } else { positions = GenerateRandomPositions(count, center, radius, _random); _logger.LogDebug("Generated {Count} positions for tasks using random fallback", positions.Count); } for (int i = 0; i < count && i < positions.Count; i++) { string taskId = $"task_{startIndex + i}"; string taskName = taskNames[(startIndex + i) % taskNames.Length]; tasks.Add(new GameTask { TaskId = taskId, Name = taskName, Location = positions[i], Type = TaskType.Instant }); } return tasks; } private List GenerateRandomPositions(int count, Position center, double radius, Random random) { var positions = new List(); for (int i = 0; i < count; i++) { // Náhodná pozice v kruhu var angle = random.NextDouble() * 2 * Math.PI; var distance = random.NextDouble() * radius * 0.8; // 80% radius aby nebyly na kraji var lat = center.Lat + (distance / 111000) * Math.Cos(angle); var lon = center.Lon + (distance / (111000 * Math.Cos(center.Lat * Math.PI / 180))) * Math.Sin(angle); positions.Add(new Position(lat, lon)); } return positions; } /// /// Generate repair station positions using map data for reachability /// public List GenerateRepairStationPositions(int count, Position center, double radius, MapData? mapData = null) { if (mapData != null && _overpassService != null && mapData.ReachablePositions.Count >= count) { // Get well-distributed reachable positions var positions = _overpassService.GetDistributedReachablePositions( mapData, count, _random, _config.MinTaskSpacingMeters * 2); if (positions.Count >= count) { _logger.LogInformation("Generated {Count} repair station positions using map data", positions.Count); return positions; } } // Fallback: opposite ends of play area var result = new List(); for (int i = 0; i < count; i++) { var angle = (2 * Math.PI * i) / count; var dist = radius * 0.7; var lat = center.Lat + (dist * Math.Cos(angle)) / 111000.0; var lon = center.Lon + (dist * Math.Sin(angle)) / (111000.0 * Math.Cos(center.Lat * Math.PI / 180)); result.Add(new Position(lat, lon)); } return result; } #endregion }