1993 lines
69 KiB
C#
1993 lines
69 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace GeoSus.Client
|
|
{
|
|
/// <summary>
|
|
/// Comprehensive headless simulator client for testing all game aspects.
|
|
/// Supports both autonomous simulation and step-by-step controlled testing.
|
|
/// </summary>
|
|
public class SimulatorClient : IDisposable
|
|
{
|
|
private readonly GameClient _client;
|
|
private readonly Random _random = new Random();
|
|
private CancellationTokenSource? _cts;
|
|
private Task? _simulationTask;
|
|
private PlayerState _myState = PlayerState.Alive;
|
|
|
|
#region Public Properties
|
|
|
|
public string ClientUuid => _client.ClientUuid;
|
|
public string DisplayName => _client.DisplayName;
|
|
public bool IsConnected => _client.IsConnected;
|
|
public LobbyState? LobbyState => _client.CurrentLobbyState;
|
|
public string? LobbyId => _client.LobbyId;
|
|
public string? JoinCode => _client.JoinCode;
|
|
public PlayerRole? Role => _client.MyRole;
|
|
public PlayerState State => _myState;
|
|
public Position Position => _client.MyPosition;
|
|
public bool IsAlive => _myState == PlayerState.Alive;
|
|
public bool IsDead => _myState == PlayerState.Dead;
|
|
public bool IsImpostor => _client.MyRole == PlayerRole.Impostor;
|
|
public bool IsCrew => _client.MyRole == PlayerRole.Crew;
|
|
|
|
// Tasks
|
|
public List<GameTask> MyTasks => _client.MyTasks;
|
|
public HashSet<string> CompletedTaskIds { get; } = new HashSet<string>();
|
|
public string? CurrentTaskId { get; private set; }
|
|
|
|
// Game state tracking
|
|
public int TotalKills { get; private set; }
|
|
public int TasksCompleted { get; private set; }
|
|
public int MeetingsAttended { get; private set; }
|
|
public int VotesCast { get; private set; }
|
|
public int BodiesReported { get; private set; }
|
|
public bool WasEjected { get; private set; }
|
|
public bool WasKilled { get; private set; }
|
|
public string? LastError { get; private set; }
|
|
public string? GameResult { get; private set; }
|
|
public string? WinningFaction { get; private set; }
|
|
public bool GameEnded => GameResult != null;
|
|
|
|
// Nearby entities
|
|
public Dictionary<string, PlayerPositionInfo> NearbyPlayers => _client.PlayerPositions;
|
|
public List<Body> NearbyBodies => _client.Bodies;
|
|
|
|
// Meeting state
|
|
public bool InMeeting { get; private set; }
|
|
public bool HasVotedThisMeeting { get; private set; }
|
|
public string? CurrentMeetingId { get; private set; }
|
|
public Position? MeetingLocation { get; private set; }
|
|
public DateTime? MeetingVotingEndTime { get; private set; }
|
|
public DateTime? MeetingDiscussionEndTime { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Returns true if the discussion phase has ended and voting can begin.
|
|
/// </summary>
|
|
public bool CanVote => InMeeting &&
|
|
(!MeetingDiscussionEndTime.HasValue || DateTime.UtcNow >= MeetingDiscussionEndTime.Value);
|
|
|
|
// Current game phase from lobby state
|
|
public string GamePhase => _client.CurrentLobbyState?.Phase.ToString() ?? "Unknown";
|
|
|
|
// Sabotage state
|
|
public bool SabotageActive { get; private set; }
|
|
public string? CurrentSabotageId { get; private set; }
|
|
public SabotageType? CurrentSabotageType { get; private set; }
|
|
public DateTime? SabotageDeadline { get; private set; }
|
|
public List<RepairStationInfo> RepairStations { get; private set; } = new List<RepairStationInfo>();
|
|
public int SabotagesStarted { get; private set; }
|
|
public int SabotagesRepaired { get; private set; }
|
|
public bool IsRepairing { get; private set; }
|
|
public string? RepairingStationId { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Returns true if comms are blocked (can't report/meeting)
|
|
/// </summary>
|
|
public bool IsCommsBlocked => SabotageActive && CurrentSabotageType == SabotageType.CommsBlackout;
|
|
|
|
/// <summary>
|
|
/// Returns true if there's a critical meltdown countdown
|
|
/// </summary>
|
|
public bool IsMeltdownActive => SabotageActive && CurrentSabotageType == SabotageType.CriticalMeltdown;
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
public event Action<string>? OnLog;
|
|
public event Action<string>? OnError;
|
|
public event Action<string, object?>? OnGameEvent;
|
|
public event Action? OnKilled;
|
|
public event Action? OnEjected;
|
|
public event Action<string>? OnGameEnded;
|
|
public event Action? OnMeetingStarted;
|
|
public event Action? OnMeetingEnded;
|
|
public event Action<SabotageType>? OnSabotageStarted;
|
|
public event Action<SabotageType>? OnSabotageRepaired;
|
|
public event Action? OnMeltdown;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public SimulatorClient(string clientUuid, string displayName)
|
|
{
|
|
_client = new GameClient(clientUuid, displayName);
|
|
|
|
_client.OnConnected += () => Log("Připojen k serveru");
|
|
_client.OnDisconnected += (reason) => Log($"Odpojen: {reason}");
|
|
_client.OnError += (error) =>
|
|
{
|
|
LastError = error;
|
|
Log($"Chyba: {error}");
|
|
OnError?.Invoke(error);
|
|
};
|
|
|
|
_client.OnMessage += HandleMessage;
|
|
_client.OnGameEvent += HandleGameEvent;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Message Handlers
|
|
|
|
private void HandleMessage(Message msg)
|
|
{
|
|
switch (msg)
|
|
{
|
|
case CreateLobbyResponse r:
|
|
if (r.Success)
|
|
Log($"Lobby vytvořeno: {r.JoinCode}");
|
|
else
|
|
LogError($"Lobby creation failed");
|
|
break;
|
|
|
|
case JoinLobbyResponse r:
|
|
if (r.Success)
|
|
Log($"Připojen do lobby: {r.LobbyId}");
|
|
else
|
|
LogError("Join lobby failed");
|
|
break;
|
|
|
|
case Ack a when !a.Success:
|
|
LastError = a.Error;
|
|
Log($"Akce zamítnuta: {a.Error}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void HandleGameEvent(GameEvent evt)
|
|
{
|
|
OnGameEvent?.Invoke(evt.EventType, evt.Payload);
|
|
|
|
switch (evt.EventType)
|
|
{
|
|
case "GameStarting":
|
|
Log("Game loading - fetching map data...");
|
|
break;
|
|
|
|
case "MapDataReady":
|
|
var mapPayload = evt.GetPayload<MapDataReadyPayload>();
|
|
var buildingCount = mapPayload?.MapData?.Buildings?.Count ?? 0;
|
|
var pathwayCount = mapPayload?.MapData?.Pathways?.Count ?? 0;
|
|
Log($"Map data received: {buildingCount} buildings, {pathwayCount} pathways - sending confirmation");
|
|
break;
|
|
|
|
case "PlayerMapDataReceived":
|
|
var progressPayload = evt.GetPayload<PlayerMapDataReceivedPayload>();
|
|
if (progressPayload != null)
|
|
{
|
|
Log($"Player {progressPayload.DisplayName} ready ({progressPayload.PlayersReady}/{progressPayload.TotalPlayers})");
|
|
}
|
|
break;
|
|
|
|
case "GameStarted":
|
|
Log("Hra začala!");
|
|
break;
|
|
|
|
case "RoleAssigned":
|
|
var rolePayload = evt.GetPayload<RoleAssignedPayload>();
|
|
if (rolePayload?.ClientUuid == ClientUuid)
|
|
{
|
|
Log($"Moje role: {rolePayload.Role}");
|
|
}
|
|
break;
|
|
|
|
case "PlayerKilled":
|
|
var killPayload = evt.GetPayload<PlayerKilledPayload>();
|
|
Log($"Hráč {killPayload?.VictimId} byl zabit");
|
|
if (killPayload?.VictimId == ClientUuid)
|
|
{
|
|
WasKilled = true;
|
|
_myState = PlayerState.Dead;
|
|
Log("BYL JSEM ZABIT!");
|
|
OnKilled?.Invoke();
|
|
}
|
|
break;
|
|
|
|
case "MeetingStarted":
|
|
var meetingPayload = evt.GetPayload<MeetingStartedPayload>();
|
|
InMeeting = true;
|
|
HasVotedThisMeeting = false;
|
|
CurrentMeetingId = meetingPayload?.MeetingId;
|
|
MeetingLocation = meetingPayload?.MeetingLocation;
|
|
MeetingVotingEndTime = meetingPayload?.VotingEndTime;
|
|
MeetingDiscussionEndTime = meetingPayload?.DiscussionEndTime;
|
|
Log($"MEETING ZAČAL! Typ: {meetingPayload?.Type}, Lokace: {MeetingLocation?.Lat:F4},{MeetingLocation?.Lon:F4}");
|
|
if (MeetingDiscussionEndTime.HasValue)
|
|
{
|
|
var discussionMs = (MeetingDiscussionEndTime.Value - DateTime.UtcNow).TotalMilliseconds;
|
|
Log($" Diskuze do: {MeetingDiscussionEndTime.Value:HH:mm:ss} ({discussionMs:F0}ms)");
|
|
}
|
|
OnMeetingStarted?.Invoke();
|
|
break;
|
|
|
|
case "VotingStarted":
|
|
Log("Hlasování začalo!");
|
|
break;
|
|
|
|
case "PlayerVoted":
|
|
var voteInfoPayload = evt.GetPayload<PlayerVotedPayload>();
|
|
Log($"Hráč {voteInfoPayload?.VoterId} hlasoval");
|
|
break;
|
|
|
|
case "VotingClosed":
|
|
var votePayload = evt.GetPayload<VotingClosedPayload>();
|
|
InMeeting = false;
|
|
CurrentMeetingId = null;
|
|
MeetingLocation = null;
|
|
MeetingVotingEndTime = null;
|
|
MeetingDiscussionEndTime = null;
|
|
if (votePayload?.EjectedPlayerId != null)
|
|
{
|
|
Log($"Hráč {votePayload.EjectedPlayerId} byl VYHOZEN! (remíza: {votePayload.WasTie})");
|
|
if (votePayload.EjectedPlayerId == ClientUuid)
|
|
{
|
|
WasEjected = true;
|
|
_myState = PlayerState.Dead;
|
|
Log("BYL JSEM VYHOZEN!");
|
|
OnEjected?.Invoke();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log("Nikdo nebyl vyhozen (skip nebo remíza)");
|
|
}
|
|
OnMeetingEnded?.Invoke();
|
|
break;
|
|
|
|
case "TaskCompleted":
|
|
var taskPayload = evt.GetPayload<TaskCompletedPayload>();
|
|
if (taskPayload?.ClientUuid == ClientUuid)
|
|
{
|
|
TasksCompleted++;
|
|
CompletedTaskIds.Add(taskPayload.TaskId);
|
|
CurrentTaskId = null;
|
|
Log($"TASK DOKONČEN: {taskPayload.TaskId} (celkem: {TasksCompleted})");
|
|
}
|
|
break;
|
|
|
|
case "GameEnded":
|
|
var endPayload = evt.GetPayload<GameEndedPayload>();
|
|
GameResult = endPayload?.Reason;
|
|
WinningFaction = endPayload?.WinningFaction;
|
|
Log($"=== HRA SKONČILA! Vítěz: {endPayload?.WinningFaction} - {endPayload?.Reason} ===");
|
|
OnGameEnded?.Invoke(endPayload?.WinningFaction ?? "Unknown");
|
|
break;
|
|
|
|
// Sabotage events
|
|
case "SabotageStarted":
|
|
var sabStartPayload = evt.GetPayload<SabotageStartedPayload>();
|
|
if (sabStartPayload != null)
|
|
{
|
|
SabotageActive = true;
|
|
CurrentSabotageId = sabStartPayload.SabotageId;
|
|
CurrentSabotageType = sabStartPayload.Type;
|
|
SabotageDeadline = sabStartPayload.Deadline;
|
|
RepairStations = sabStartPayload.RepairStations;
|
|
|
|
Log($"⚠ SABOTÁŽ SPUŠTĚNA: {sabStartPayload.Type}!");
|
|
if (sabStartPayload.Deadline.HasValue)
|
|
{
|
|
var remaining = (sabStartPayload.Deadline.Value - DateTime.UtcNow).TotalSeconds;
|
|
Log($" ⏱ DEADLINE: {remaining:F0}s - musíte opravit nebo prohrajete!");
|
|
}
|
|
foreach (var station in sabStartPayload.RepairStations)
|
|
{
|
|
Log($" 📍 Stanice {station.Name}: {station.Location.Lat:F4},{station.Location.Lon:F4}");
|
|
}
|
|
OnSabotageStarted?.Invoke(sabStartPayload.Type);
|
|
}
|
|
break;
|
|
|
|
case "RepairStarted":
|
|
var repStartPayload = evt.GetPayload<RepairStartedPayload>();
|
|
if (repStartPayload?.PlayerId == ClientUuid)
|
|
{
|
|
IsRepairing = true;
|
|
RepairingStationId = repStartPayload.StationId;
|
|
Log($"🔧 Zahájil jsem opravu stanice {repStartPayload.StationId}");
|
|
}
|
|
else
|
|
{
|
|
Log($"🔧 Hráč {repStartPayload?.PlayerId} opravuje {repStartPayload?.StationId}");
|
|
}
|
|
break;
|
|
|
|
case "RepairStopped":
|
|
var repStopPayload = evt.GetPayload<RepairStoppedPayload>();
|
|
if (repStopPayload?.PlayerId == ClientUuid)
|
|
{
|
|
IsRepairing = false;
|
|
RepairingStationId = null;
|
|
Log($"❌ Oprava přerušena: {repStopPayload.StationId}");
|
|
}
|
|
break;
|
|
|
|
case "SabotageRepaired":
|
|
case "SabotageExpired":
|
|
var sabRepPayload = evt.GetPayload<SabotageRepairedPayload>();
|
|
if (sabRepPayload != null)
|
|
{
|
|
SabotageActive = false;
|
|
CurrentSabotageId = null;
|
|
CurrentSabotageType = null;
|
|
SabotageDeadline = null;
|
|
RepairStations.Clear();
|
|
IsRepairing = false;
|
|
RepairingStationId = null;
|
|
SabotagesRepaired++;
|
|
|
|
var repairers = sabRepPayload.RepairerIds.Count > 0
|
|
? string.Join(", ", sabRepPayload.RepairerIds)
|
|
: "auto-expire";
|
|
Log($"✅ SABOTÁŽ OPRAVENA: {sabRepPayload.Type} (opravili: {repairers})");
|
|
OnSabotageRepaired?.Invoke(sabRepPayload.Type);
|
|
}
|
|
break;
|
|
|
|
case "SabotageMeltdown":
|
|
var meltdownPayload = evt.GetPayload<SabotageMeltdownPayload>();
|
|
Log($"💥 MELTDOWN! Sabotáž nebyla opravena včas - Impostoři vyhráli!");
|
|
OnMeltdown?.Invoke();
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Connection & Lobby
|
|
|
|
public async Task<bool> ConnectAsync(string host, int port)
|
|
{
|
|
return await _client.ConnectAsync(host, port);
|
|
}
|
|
|
|
public void Disconnect()
|
|
{
|
|
StopSimulation();
|
|
_client.Disconnect();
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
_client.Update();
|
|
}
|
|
|
|
public void CreateLobby(Position center, int impostorCount = 1, int taskCount = 5, string? password = null)
|
|
{
|
|
_client.CreateLobby(center, impostorCount, taskCount, password);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Async wrapper for CreateLobby - waits for lobby to be created
|
|
/// </summary>
|
|
public async Task<bool> CreateLobbyAsync(string? password, Position center, double radius = 500, int impostorCount = 1, int taskCount = 5)
|
|
{
|
|
_client.CreateLobby(center, impostorCount, taskCount, password, radius);
|
|
|
|
// Wait for lobby creation response
|
|
for (int i = 0; i < 50; i++) // 5 seconds timeout
|
|
{
|
|
Update();
|
|
if (!string.IsNullOrEmpty(JoinCode))
|
|
return true;
|
|
if (LastError != null && LastError.Contains("lobby"))
|
|
return false;
|
|
await Task.Delay(100);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void JoinLobby(string joinCode, string? password = null)
|
|
{
|
|
_client.JoinLobby(joinCode, password);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Async wrapper for JoinLobby - waits for join confirmation
|
|
/// </summary>
|
|
public async Task<bool> JoinLobbyAsync(string joinCode, string? password = null)
|
|
{
|
|
_client.JoinLobby(joinCode, password);
|
|
|
|
// Wait for join response
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
Update();
|
|
if (!string.IsNullOrEmpty(LobbyId))
|
|
return true;
|
|
if (LastError != null && LastError.Contains("join"))
|
|
return false;
|
|
await Task.Delay(100);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void LeaveLobby()
|
|
{
|
|
_client.LeaveLobby();
|
|
}
|
|
|
|
public void StartGame()
|
|
{
|
|
_client.StartGame();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Async wrapper for StartGame - waits for game to start
|
|
/// </summary>
|
|
public async Task<bool> StartGameAsync()
|
|
{
|
|
_client.StartGame();
|
|
|
|
// Wait for game start
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
Update();
|
|
if (Role.HasValue)
|
|
return true;
|
|
await Task.Delay(100);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Movement
|
|
|
|
public void MoveTo(Position position)
|
|
{
|
|
_client.UpdatePosition(position);
|
|
}
|
|
|
|
public void MoveTowards(Position target, double maxDistanceMeters)
|
|
{
|
|
var current = Position;
|
|
var distance = current.DistanceTo(target);
|
|
|
|
if (distance <= maxDistanceMeters)
|
|
{
|
|
MoveTo(target);
|
|
}
|
|
else
|
|
{
|
|
var ratio = maxDistanceMeters / distance;
|
|
var newPos = new Position(
|
|
current.Lat + (target.Lat - current.Lat) * ratio,
|
|
current.Lon + (target.Lon - current.Lon) * ratio
|
|
);
|
|
MoveTo(newPos);
|
|
}
|
|
}
|
|
|
|
public Position GetRandomPositionNear(Position center, double radiusMeters)
|
|
{
|
|
var angle = _random.NextDouble() * 2 * Math.PI;
|
|
var distance = _random.NextDouble() * radiusMeters;
|
|
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);
|
|
return new Position(lat, lon);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Kill Actions (Impostor)
|
|
|
|
public bool TryKill(string targetUuid)
|
|
{
|
|
if (!IsImpostor || !IsAlive)
|
|
{
|
|
Log("Nemohu zabíjet - nejsem živý impostor");
|
|
return false;
|
|
}
|
|
|
|
Log($">>> POKUS O ZABITÍ: {targetUuid}");
|
|
_client.Kill(targetUuid);
|
|
TotalKills++;
|
|
return true;
|
|
}
|
|
|
|
public string? FindKillTarget(double maxDistance = 5.0)
|
|
{
|
|
return _client.FindNearbyPlayer(maxDistance, aliveOnly: true);
|
|
}
|
|
|
|
public bool TryKillNearby(double maxDistance = 5.0)
|
|
{
|
|
var target = FindKillTarget(maxDistance);
|
|
if (target != null)
|
|
{
|
|
return TryKill(target);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Report & Meeting Actions
|
|
|
|
public bool TryReportBody(string bodyId)
|
|
{
|
|
if (!IsAlive)
|
|
{
|
|
Log("Nemohu reportovat - jsem mrtvý");
|
|
return false;
|
|
}
|
|
|
|
Log($">>> REPORTUJI TĚLO: {bodyId}");
|
|
_client.ReportBody(bodyId);
|
|
BodiesReported++;
|
|
return true;
|
|
}
|
|
|
|
public Body? FindNearbyBody(double maxDistance = 5.0)
|
|
{
|
|
return _client.FindNearbyBody(maxDistance);
|
|
}
|
|
|
|
public bool TryReportNearbyBody(double maxDistance = 5.0)
|
|
{
|
|
var body = FindNearbyBody(maxDistance);
|
|
if (body != null)
|
|
{
|
|
return TryReportBody(body.BodyId);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public bool TryCallEmergencyMeeting()
|
|
{
|
|
if (!IsAlive)
|
|
{
|
|
Log("Nemohu svolat meeting - jsem mrtvý");
|
|
return false;
|
|
}
|
|
|
|
Log(">>> SVOLÁVÁM EMERGENCY MEETING!");
|
|
_client.CallEmergencyMeeting();
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Voting Actions
|
|
|
|
public bool TryVote(string? targetUuid)
|
|
{
|
|
if (!IsAlive)
|
|
{
|
|
Log("Nemohu hlasovat - jsem mrtvý");
|
|
return false;
|
|
}
|
|
|
|
if (!InMeeting)
|
|
{
|
|
Log("Nemohu hlasovat - není meeting");
|
|
return false;
|
|
}
|
|
|
|
var voteTarget = targetUuid ?? "SKIP";
|
|
Log($">>> HLASUJI PRO: {voteTarget}");
|
|
_client.Vote(targetUuid);
|
|
VotesCast++;
|
|
HasVotedThisMeeting = true;
|
|
MeetingsAttended++;
|
|
return true;
|
|
}
|
|
|
|
public bool TryVoteSkip()
|
|
{
|
|
return TryVote(null);
|
|
}
|
|
|
|
public bool TryVoteRandom()
|
|
{
|
|
if (!InMeeting || !IsAlive) return false;
|
|
|
|
// Pick a random alive player (or skip)
|
|
var alivePlayers = NearbyPlayers.Values
|
|
.Where(p => p.State == PlayerState.Alive && p.ClientUuid != ClientUuid)
|
|
.ToList();
|
|
|
|
if (alivePlayers.Count == 0 || _random.NextDouble() < 0.3)
|
|
{
|
|
return TryVoteSkip();
|
|
}
|
|
|
|
var target = alivePlayers[_random.Next(alivePlayers.Count)];
|
|
return TryVote(target.ClientUuid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vote for the player with the most suspicion (for crew) or a random crew (for impostor)
|
|
/// </summary>
|
|
public bool TryVoteSmart()
|
|
{
|
|
if (!InMeeting || !IsAlive) return false;
|
|
|
|
var alivePlayers = NearbyPlayers.Values
|
|
.Where(p => p.State == PlayerState.Alive && p.ClientUuid != ClientUuid)
|
|
.ToList();
|
|
|
|
if (alivePlayers.Count == 0)
|
|
{
|
|
return TryVoteSkip();
|
|
}
|
|
|
|
// Impostors vote randomly among non-impostors or skip
|
|
if (IsImpostor)
|
|
{
|
|
if (_random.NextDouble() < 0.5)
|
|
{
|
|
return TryVoteSkip();
|
|
}
|
|
var target = alivePlayers[_random.Next(alivePlayers.Count)];
|
|
return TryVote(target.ClientUuid);
|
|
}
|
|
|
|
// Crew votes randomly for now (could be smarter with suspicion tracking)
|
|
if (_random.NextDouble() < 0.2)
|
|
{
|
|
return TryVoteSkip();
|
|
}
|
|
var crewTarget = alivePlayers[_random.Next(alivePlayers.Count)];
|
|
return TryVote(crewTarget.ClientUuid);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Task Actions
|
|
|
|
public bool TryCompleteTask(string taskId)
|
|
{
|
|
if (IsImpostor)
|
|
{
|
|
Log($"Nemohu dělat tasky - jsem impostor");
|
|
return false;
|
|
}
|
|
|
|
if (CompletedTaskIds.Contains(taskId))
|
|
{
|
|
Log($"Task {taskId} již dokončen");
|
|
return false;
|
|
}
|
|
|
|
Log($">>> DOKONČUJI TASK: {taskId}");
|
|
_client.CompleteTask(taskId);
|
|
return true;
|
|
}
|
|
|
|
public GameTask? FindNearbyTask(double maxDistance = 5.0)
|
|
{
|
|
foreach (var task in MyTasks)
|
|
{
|
|
if (CompletedTaskIds.Contains(task.TaskId)) continue;
|
|
|
|
if (Position.DistanceTo(task.Location) <= maxDistance)
|
|
{
|
|
return task;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public GameTask? GetNextIncompleteTask()
|
|
{
|
|
return MyTasks.FirstOrDefault(t => !CompletedTaskIds.Contains(t.TaskId));
|
|
}
|
|
|
|
public int GetRemainingTaskCount()
|
|
{
|
|
return MyTasks.Count - CompletedTaskIds.Count;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Sabotage Actions (Impostor)
|
|
|
|
/// <summary>
|
|
/// Start a sabotage (impostor only)
|
|
/// </summary>
|
|
public bool TrySabotage(SabotageType sabotageType)
|
|
{
|
|
if (!IsImpostor)
|
|
{
|
|
Log("Nemohu sabotovat - nejsem impostor");
|
|
return false;
|
|
}
|
|
|
|
if (!IsAlive)
|
|
{
|
|
Log("Nemohu sabotovat - jsem mrtvý");
|
|
return false;
|
|
}
|
|
|
|
if (SabotageActive)
|
|
{
|
|
Log($"Nemohu sabotovat - již probíhá sabotáž: {CurrentSabotageType}");
|
|
return false;
|
|
}
|
|
|
|
if (InMeeting)
|
|
{
|
|
Log("Nemohu sabotovat - probíhá meeting");
|
|
return false;
|
|
}
|
|
|
|
Log($">>> SPOUŠTÍM SABOTÁŽ: {sabotageType}");
|
|
_client.Send(new StartSabotage { SabotageType = sabotageType });
|
|
SabotagesStarted++;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start repairing at a repair station (crew or impostor can repair)
|
|
/// </summary>
|
|
public bool TryStartRepair(string stationId)
|
|
{
|
|
if (!IsAlive)
|
|
{
|
|
Log("Nemohu opravovat - jsem mrtvý");
|
|
return false;
|
|
}
|
|
|
|
if (!SabotageActive)
|
|
{
|
|
Log("Nemohu opravovat - není aktivní sabotáž");
|
|
return false;
|
|
}
|
|
|
|
if (IsRepairing)
|
|
{
|
|
Log($"Již opravuji stanici: {RepairingStationId}");
|
|
return false;
|
|
}
|
|
|
|
var station = RepairStations.FirstOrDefault(s => s.StationId == stationId);
|
|
if (station == null)
|
|
{
|
|
Log($"Opravná stanice {stationId} neexistuje");
|
|
return false;
|
|
}
|
|
|
|
var distance = Position.DistanceTo(station.Location);
|
|
if (distance > 5.0)
|
|
{
|
|
Log($"Opravná stanice {stationId} je příliš daleko: {distance:F1}m");
|
|
return false;
|
|
}
|
|
|
|
Log($">>> ZAČÍNÁM OPRAVU stanice: {stationId}");
|
|
_client.Send(new ActivateRepairStation { StationId = stationId });
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stop repairing current station
|
|
/// </summary>
|
|
public bool TryStopRepair()
|
|
{
|
|
if (!IsRepairing || RepairingStationId == null)
|
|
{
|
|
Log("Nejsem u opravné stanice");
|
|
return false;
|
|
}
|
|
|
|
Log($">>> UKONČUJI OPRAVU stanice: {RepairingStationId}");
|
|
_client.Send(new DeactivateRepairStation { StationId = RepairingStationId });
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find nearest repair station for current sabotage
|
|
/// </summary>
|
|
public RepairStationInfo? FindNearestRepairStation(double maxDistance = double.MaxValue)
|
|
{
|
|
if (!SabotageActive) return null;
|
|
|
|
RepairStationInfo? nearest = null;
|
|
double nearestDist = maxDistance;
|
|
|
|
foreach (var station in RepairStations)
|
|
{
|
|
if (station.IsRepaired) continue;
|
|
|
|
var dist = Position.DistanceTo(station.Location);
|
|
if (dist < nearestDist)
|
|
{
|
|
nearestDist = dist;
|
|
nearest = station;
|
|
}
|
|
}
|
|
|
|
return nearest;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Move to nearest repair station
|
|
/// </summary>
|
|
public bool MoveTowardsNearestRepairStation(double speed = 1.0)
|
|
{
|
|
var station = FindNearestRepairStation();
|
|
if (station == null) return false;
|
|
|
|
MoveTowards(station.Location, speed);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if at repair station and can start repair
|
|
/// </summary>
|
|
public bool IsAtRepairStation(string stationId, double maxDistance = 5.0)
|
|
{
|
|
var station = RepairStations.FirstOrDefault(s => s.StationId == stationId);
|
|
if (station == null) return false;
|
|
|
|
return Position.DistanceTo(station.Location) <= maxDistance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Automatic repair: find nearest station, move to it, and start repair
|
|
/// </summary>
|
|
public bool TryAutoRepair()
|
|
{
|
|
if (!SabotageActive) return false;
|
|
if (IsRepairing) return false;
|
|
|
|
var station = FindNearestRepairStation(5.0);
|
|
if (station != null && !station.IsRepaired)
|
|
{
|
|
return TryStartRepair(station.StationId);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Autonomous Simulation
|
|
|
|
public void StartSimulation()
|
|
{
|
|
if (_simulationTask != null) return;
|
|
|
|
_cts = new CancellationTokenSource();
|
|
_simulationTask = Task.Run(() => SimulationLoopAsync(_cts.Token));
|
|
Log("Simulace spuštěna");
|
|
}
|
|
|
|
public void StopSimulation()
|
|
{
|
|
if (_cts == null) return;
|
|
|
|
_cts.Cancel();
|
|
try { _simulationTask?.Wait(1000); } catch { }
|
|
_simulationTask = null;
|
|
_cts = null;
|
|
Log("Simulace zastavena");
|
|
}
|
|
|
|
private async Task SimulationLoopAsync(CancellationToken ct)
|
|
{
|
|
var center = LobbyState?.PlayAreaCenter ?? new Position(50.0, 14.0);
|
|
var radius = LobbyState?.PlayAreaRadius ?? 500;
|
|
|
|
MoveTo(center);
|
|
|
|
while (!ct.IsCancellationRequested && !GameEnded)
|
|
{
|
|
try
|
|
{
|
|
Update();
|
|
|
|
// In meeting - handle voting
|
|
if (InMeeting)
|
|
{
|
|
// First move to meeting location
|
|
if (MeetingLocation.HasValue && IsAlive)
|
|
{
|
|
var meetLoc = MeetingLocation.Value;
|
|
var distToMeeting = Position.DistanceTo(meetLoc);
|
|
if (distToMeeting > 5)
|
|
{
|
|
MoveTowards(meetLoc, 20);
|
|
await Task.Delay(200, ct);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (IsAlive && !HasVotedThisMeeting)
|
|
{
|
|
// Wait a bit before voting (simulate discussion)
|
|
await Task.Delay(500 + _random.Next(1500), ct);
|
|
Update();
|
|
|
|
if (InMeeting && !HasVotedThisMeeting)
|
|
{
|
|
TryVoteSmart();
|
|
}
|
|
}
|
|
|
|
// Wait for meeting to end
|
|
await Task.Delay(300, ct);
|
|
continue;
|
|
}
|
|
|
|
// Impostor logic
|
|
if (IsImpostor && IsAlive)
|
|
{
|
|
await ImpostorActionAsync(center, radius, ct);
|
|
}
|
|
// Alive Crew logic
|
|
else if (IsCrew && IsAlive)
|
|
{
|
|
await CrewActionAsync(center, radius, ct);
|
|
}
|
|
// Dead player (ghost) - can still do tasks
|
|
else if (IsCrew && IsDead)
|
|
{
|
|
await GhostTaskActionAsync(center, radius, ct);
|
|
}
|
|
// Dead impostor - just watch
|
|
else if (IsImpostor && IsDead)
|
|
{
|
|
await Task.Delay(1000, ct);
|
|
}
|
|
|
|
await Task.Delay(300, ct);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"Simulation error: {ex.Message}");
|
|
await Task.Delay(1000, ct);
|
|
}
|
|
}
|
|
|
|
Log("Simulace dokončena");
|
|
}
|
|
|
|
private async Task ImpostorActionAsync(Position center, double radius, CancellationToken ct)
|
|
{
|
|
// Try to kill nearby player
|
|
var target = FindKillTarget(8.0);
|
|
if (target != null)
|
|
{
|
|
// Higher chance to kill if alone with victim
|
|
var nearbyCount = NearbyPlayers.Values.Count(p =>
|
|
p.State == PlayerState.Alive &&
|
|
p.ClientUuid != ClientUuid &&
|
|
Position.DistanceTo(p.Position) < 20);
|
|
|
|
var killChance = nearbyCount <= 1 ? 0.9 : 0.3;
|
|
|
|
if (_random.NextDouble() < killChance)
|
|
{
|
|
TryKill(target);
|
|
await Task.Delay(300, ct);
|
|
|
|
// Move away from body
|
|
var escapePos = GetRandomPositionNear(Position, 30);
|
|
MoveTowards(escapePos, 15);
|
|
await Task.Delay(500, ct);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Wander around looking for isolated targets
|
|
var wanderPos = GetRandomPositionNear(center, radius * 0.6);
|
|
MoveTowards(wanderPos, 5);
|
|
}
|
|
|
|
private async Task CrewActionAsync(Position center, double radius, CancellationToken ct)
|
|
{
|
|
// Priority 1: Report nearby bodies
|
|
var body = FindNearbyBody(8.0);
|
|
if (body != null)
|
|
{
|
|
Log($"NAŠEL JSEM TĚLO: {body.BodyId}");
|
|
// Move closer if needed
|
|
if (Position.DistanceTo(body.Location) > 3)
|
|
{
|
|
MoveTowards(body.Location, 3);
|
|
await Task.Delay(200, ct);
|
|
}
|
|
TryReportBody(body.BodyId);
|
|
await Task.Delay(500, ct);
|
|
return;
|
|
}
|
|
|
|
// Priority 2: Do tasks (instant completion)
|
|
var nearbyTask = FindNearbyTask(5.0);
|
|
if (nearbyTask != null)
|
|
{
|
|
TryCompleteTask(nearbyTask.TaskId);
|
|
await Task.Delay(500, ct);
|
|
return;
|
|
}
|
|
|
|
// Priority 3: Move to next task
|
|
var nextTask = GetNextIncompleteTask();
|
|
if (nextTask != null)
|
|
{
|
|
MoveTowards(nextTask.Location, 5);
|
|
}
|
|
else
|
|
{
|
|
// All tasks done, wander
|
|
var wanderPos = GetRandomPositionNear(center, radius * 0.5);
|
|
MoveTowards(wanderPos, 3);
|
|
}
|
|
}
|
|
|
|
private async Task GhostTaskActionAsync(Position center, double radius, CancellationToken ct)
|
|
{
|
|
// Ghosts can complete tasks faster (no danger)
|
|
var nearbyTask = FindNearbyTask(10.0);
|
|
if (nearbyTask != null)
|
|
{
|
|
TryCompleteTask(nearbyTask.TaskId);
|
|
await Task.Delay(100, ct);
|
|
return;
|
|
}
|
|
|
|
// Move to next task
|
|
var nextTask = GetNextIncompleteTask();
|
|
if (nextTask != null)
|
|
{
|
|
// Ghosts move much faster
|
|
MoveTowards(nextTask.Location, 20);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Logging
|
|
|
|
private void Log(string message)
|
|
{
|
|
OnLog?.Invoke(message);
|
|
}
|
|
|
|
private void LogError(string message)
|
|
{
|
|
LastError = message;
|
|
OnError?.Invoke(message);
|
|
Log($"ERROR: {message}");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Stats
|
|
|
|
public string GetStats()
|
|
{
|
|
return $"[{DisplayName}] Role={Role}, State={State}, Kills={TotalKills}, Tasks={TasksCompleted}/{MyTasks.Count}, " +
|
|
$"Reports={BodiesReported}, Votes={VotesCast}, Meetings={MeetingsAttended}, " +
|
|
$"Killed={WasKilled}, Ejected={WasEjected}";
|
|
}
|
|
|
|
public void PrintDetailedStats()
|
|
{
|
|
Log("========== DETAILED STATS ==========");
|
|
Log($" Display Name: {DisplayName}");
|
|
Log($" Client UUID: {ClientUuid}");
|
|
Log($" Role: {Role}");
|
|
Log($" Final State: {State}");
|
|
Log($" --- Actions ---");
|
|
Log($" Kills Attempted: {TotalKills}");
|
|
Log($" Tasks Completed: {TasksCompleted}/{MyTasks.Count}");
|
|
Log($" Bodies Reported: {BodiesReported}");
|
|
Log($" Votes Cast: {VotesCast}");
|
|
Log($" Meetings Attended: {MeetingsAttended}");
|
|
Log($" --- Fate ---");
|
|
Log($" Was Killed: {WasKilled}");
|
|
Log($" Was Ejected: {WasEjected}");
|
|
Log($" --- Game Result ---");
|
|
Log($" Winning Faction: {WinningFaction}");
|
|
Log($" Result: {GameResult}");
|
|
Log("=====================================");
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void Dispose()
|
|
{
|
|
StopSimulation();
|
|
_client.Dispose();
|
|
}
|
|
|
|
#region Map Data Access
|
|
|
|
/// <summary>
|
|
/// Get the map data payload from the current lobby state (if available)
|
|
/// </summary>
|
|
public MapDataPayload? MapData => _client.CurrentLobbyState?.MapData;
|
|
|
|
/// <summary>
|
|
/// Check if map data is available for the current lobby
|
|
/// </summary>
|
|
public bool HasMapData => MapData != null;
|
|
|
|
/// <summary>
|
|
/// Check if map data loading is complete (true if loaded or Overpass disabled)
|
|
/// </summary>
|
|
public bool IsMapDataReady => _client.CurrentLobbyState?.MapDataReady ?? false;
|
|
|
|
/// <summary>
|
|
/// Get the play area center from lobby state
|
|
/// </summary>
|
|
public Position? PlayAreaCenter => _client.CurrentLobbyState?.PlayAreaCenter;
|
|
|
|
/// <summary>
|
|
/// Get the play area radius from lobby state
|
|
/// </summary>
|
|
public double PlayAreaRadius => _client.CurrentLobbyState?.PlayAreaRadius ?? 500;
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Comprehensive test suite for Overpass API and reachability testing
|
|
/// </summary>
|
|
public class OverpassApiTests
|
|
{
|
|
private readonly string _serverHost;
|
|
private readonly int _serverPort;
|
|
private readonly Action<string>? _logger;
|
|
|
|
public OverpassApiTests(string serverHost = "localhost", int serverPort = 7777, Action<string>? logger = null)
|
|
{
|
|
_serverHost = serverHost;
|
|
_serverPort = serverPort;
|
|
_logger = logger;
|
|
}
|
|
|
|
private void Log(string message)
|
|
{
|
|
_logger?.Invoke(message);
|
|
Console.WriteLine(message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run all Overpass API tests
|
|
/// </summary>
|
|
public async Task<OverpassTestResults> RunAllTestsAsync(Position testCenter, double testRadius = 500)
|
|
{
|
|
var results = new OverpassTestResults();
|
|
|
|
Log("═══════════════════════════════════════════════════════════════");
|
|
Log(" OVERPASS API & REACHABILITY TEST SUITE");
|
|
Log("═══════════════════════════════════════════════════════════════");
|
|
Log($"Test Center: {testCenter.Lat:F6}, {testCenter.Lon:F6}");
|
|
Log($"Test Radius: {testRadius}m");
|
|
Log("");
|
|
|
|
// Test 1: Create lobby and verify map data is fetched
|
|
Log("TEST 1: Map Data Fetch on Lobby Creation");
|
|
Log("─────────────────────────────────────────");
|
|
results.MapDataFetchTest = await TestMapDataFetchAsync(testCenter, testRadius);
|
|
LogTestResult("Map Data Fetch", results.MapDataFetchTest);
|
|
await Task.Delay(1000); // Delay between tests
|
|
|
|
// Test 2: Verify map data structure
|
|
Log("");
|
|
Log("TEST 2: Map Data Structure Validation");
|
|
Log("─────────────────────────────────────────");
|
|
results.MapDataStructureTest = await TestMapDataStructureAsync(testCenter, testRadius);
|
|
LogTestResult("Map Data Structure", results.MapDataStructureTest);
|
|
await Task.Delay(1000); // Delay between tests
|
|
|
|
// Test 3: Verify task positions are on reachable paths
|
|
Log("");
|
|
Log("TEST 3: Task Position Reachability");
|
|
Log("─────────────────────────────────────────");
|
|
results.TaskReachabilityTest = await TestTaskReachabilityAsync(testCenter, testRadius);
|
|
LogTestResult("Task Reachability", results.TaskReachabilityTest);
|
|
await Task.Delay(1000); // Delay between tests
|
|
|
|
// Test 4: Verify repair station positions are reachable
|
|
Log("");
|
|
Log("TEST 4: Repair Station Reachability (Sabotage)");
|
|
Log("─────────────────────────────────────────");
|
|
results.RepairStationReachabilityTest = await TestRepairStationReachabilityAsync(testCenter, testRadius);
|
|
LogTestResult("Repair Station Reachability", results.RepairStationReachabilityTest);
|
|
await Task.Delay(1000); // Delay between tests
|
|
|
|
// Test 5: Verify positions are within play area
|
|
Log("");
|
|
Log("TEST 5: Play Area Boundary Validation");
|
|
Log("─────────────────────────────────────────");
|
|
results.PlayAreaBoundaryTest = await TestPlayAreaBoundaryAsync(testCenter, testRadius);
|
|
LogTestResult("Play Area Boundary", results.PlayAreaBoundaryTest);
|
|
await Task.Delay(1000); // Delay between tests
|
|
|
|
// Test 6: Test multiple lobby creations for consistency
|
|
Log("");
|
|
Log("TEST 6: Map Data Consistency Across Lobbies");
|
|
Log("─────────────────────────────────────────");
|
|
results.ConsistencyTest = await TestMapDataConsistencyAsync(testCenter, testRadius);
|
|
LogTestResult("Map Data Consistency", results.ConsistencyTest);
|
|
|
|
// Summary
|
|
Log("");
|
|
Log("═══════════════════════════════════════════════════════════════");
|
|
Log(" TEST SUMMARY");
|
|
Log("═══════════════════════════════════════════════════════════════");
|
|
results.PrintSummary(Log);
|
|
|
|
return results;
|
|
}
|
|
|
|
private void LogTestResult(string testName, TestResult result)
|
|
{
|
|
var status = result.Passed ? "✓ PASS" : "✗ FAIL";
|
|
Log($" [{status}] {testName}");
|
|
if (!string.IsNullOrEmpty(result.Details))
|
|
{
|
|
foreach (var line in result.Details.Split('\n'))
|
|
{
|
|
Log($" {line}");
|
|
}
|
|
}
|
|
if (!result.Passed && !string.IsNullOrEmpty(result.Error))
|
|
{
|
|
Log($" ERROR: {result.Error}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wait for map data to be loaded with polling and timeout
|
|
/// Note: Map data is now fetched when game starts, not on lobby creation
|
|
/// </summary>
|
|
private async Task<bool> WaitForMapDataAsync(SimulatorClient client, SimulatorClient? otherClient, int timeoutMs = 10000, int pollIntervalMs = 500)
|
|
{
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
|
|
while (sw.ElapsedMilliseconds < timeoutMs)
|
|
{
|
|
// Update client to receive any pending messages
|
|
client.Update();
|
|
otherClient?.Update();
|
|
|
|
// Check if client has map data loaded
|
|
if (client.HasMapData)
|
|
{
|
|
Log($" Map data loaded after {sw.ElapsedMilliseconds}ms");
|
|
return true;
|
|
}
|
|
|
|
// Also check if MapDataReady indicates no data expected (Overpass disabled)
|
|
if (client.IsMapDataReady && !client.HasMapData)
|
|
{
|
|
Log($" Map data not available (Overpass may be disabled)");
|
|
return false;
|
|
}
|
|
|
|
await Task.Delay(pollIntervalMs);
|
|
}
|
|
|
|
Log($" Timeout waiting for map data ({timeoutMs}ms)");
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wait for game to start after map data is received (Loading phase complete)
|
|
/// </summary>
|
|
private async Task<bool> WaitForGameStartAfterMapDataAsync(SimulatorClient owner, SimulatorClient player, int timeoutMs = 20000, int pollIntervalMs = 500)
|
|
{
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
|
|
while (sw.ElapsedMilliseconds < timeoutMs)
|
|
{
|
|
owner.Update();
|
|
player.Update();
|
|
|
|
// Game is fully started when phase is Playing and roles are assigned
|
|
if (owner.GamePhase == "Playing" && owner.Role.HasValue)
|
|
{
|
|
Log($" Game started after {sw.ElapsedMilliseconds}ms");
|
|
return true;
|
|
}
|
|
|
|
await Task.Delay(pollIntervalMs);
|
|
}
|
|
|
|
Log($" Timeout waiting for game start ({timeoutMs}ms) - Phase: {owner.GamePhase}");
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start a game and wait for map data to be loaded
|
|
/// The new flow is: StartGame -> Loading phase -> MapData fetched -> All confirm -> Playing
|
|
/// </summary>
|
|
private async Task<bool> StartGameAndWaitForMapDataAsync(SimulatorClient owner, SimulatorClient player, int timeoutMs = 25000)
|
|
{
|
|
Log(" Starting game (this will trigger map data fetch)...");
|
|
owner.StartGame();
|
|
|
|
// Wait for map data first (update both clients so both send confirmations)
|
|
if (!await WaitForMapDataAsync(owner, player, timeoutMs))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Ensure both clients update to send MapDataReceived confirmation
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
owner.Update();
|
|
player.Update();
|
|
await Task.Delay(100);
|
|
}
|
|
|
|
// Wait for game to actually start (after all confirmations)
|
|
return await WaitForGameStartAfterMapDataAsync(owner, player, 10000);
|
|
}
|
|
|
|
private async Task<TestResult> TestMapDataFetchAsync(Position center, double radius)
|
|
{
|
|
var result = new TestResult { TestName = "MapDataFetch" };
|
|
SimulatorClient? owner = null;
|
|
SimulatorClient? player = null;
|
|
|
|
try
|
|
{
|
|
// Create owner and player (need 2 players to start game)
|
|
owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "MapOwner");
|
|
player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "MapPlayer");
|
|
|
|
// Connect owner
|
|
Log(" Connecting owner to server...");
|
|
if (!await owner.ConnectAsync(_serverHost, _serverPort))
|
|
{
|
|
result.Error = $"Failed to connect owner to server at {_serverHost}:{_serverPort}";
|
|
return result;
|
|
}
|
|
|
|
// Create lobby with specific center
|
|
Log($" Creating lobby at {center.Lat:F6}, {center.Lon:F6} with radius {radius}m...");
|
|
if (!await owner.CreateLobbyAsync(null, center, radius))
|
|
{
|
|
result.Error = $"Failed to create lobby: {owner.LastError ?? "Unknown error"}";
|
|
return result;
|
|
}
|
|
Log($" Lobby created: {owner.JoinCode}");
|
|
|
|
// Connect player
|
|
Log(" Connecting player to server...");
|
|
if (!await player.ConnectAsync(_serverHost, _serverPort))
|
|
{
|
|
result.Error = "Failed to connect player";
|
|
return result;
|
|
}
|
|
|
|
// Join player to lobby
|
|
if (!await player.JoinLobbyAsync(owner.JoinCode!))
|
|
{
|
|
result.Error = "Failed to join player to lobby";
|
|
return result;
|
|
}
|
|
|
|
// Start game - this triggers map data fetch
|
|
Log(" Starting game (triggers Overpass API fetch)...");
|
|
if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
|
|
{
|
|
result.Error = "Timeout waiting for map data (Overpass API may be slow or unavailable)";
|
|
return result;
|
|
}
|
|
|
|
if (owner.HasMapData)
|
|
{
|
|
var mapData = owner.MapData!;
|
|
var buildings = mapData.GetBuildings();
|
|
var pathways = mapData.GetPathways();
|
|
var areas = mapData.GetAreas();
|
|
var pois = mapData.GetPOIs();
|
|
result.Passed = true;
|
|
result.Details = $"Buildings: {buildings.Count}\n" +
|
|
$"Pathways: {pathways.Count}\n" +
|
|
$"Areas: {areas.Count}\n" +
|
|
$"POIs: {pois.Count}";
|
|
}
|
|
else
|
|
{
|
|
result.Error = "Map data was not received after game start";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Error = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
owner?.Dispose();
|
|
player?.Dispose();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task<TestResult> TestMapDataStructureAsync(Position center, double radius)
|
|
{
|
|
var result = new TestResult { TestName = "MapDataStructure" };
|
|
SimulatorClient? owner = null;
|
|
SimulatorClient? player = null;
|
|
|
|
try
|
|
{
|
|
owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "StructOwner");
|
|
player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "StructPlayer");
|
|
|
|
if (!await owner.ConnectAsync(_serverHost, _serverPort))
|
|
{
|
|
result.Error = "Failed to connect owner";
|
|
return result;
|
|
}
|
|
|
|
if (!await owner.CreateLobbyAsync(null, center, radius))
|
|
{
|
|
result.Error = "Failed to create lobby";
|
|
return result;
|
|
}
|
|
|
|
if (!await player.ConnectAsync(_serverHost, _serverPort))
|
|
{
|
|
result.Error = "Failed to connect player";
|
|
return result;
|
|
}
|
|
|
|
if (!await player.JoinLobbyAsync(owner.JoinCode!))
|
|
{
|
|
result.Error = "Failed to join player to lobby";
|
|
return result;
|
|
}
|
|
|
|
// Start game to trigger map data fetch
|
|
Log(" Starting game (triggers Overpass API fetch)...");
|
|
if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
|
|
{
|
|
result.Error = "Timeout waiting for map data";
|
|
return result;
|
|
}
|
|
|
|
if (!owner.HasMapData)
|
|
{
|
|
result.Error = "No map data available";
|
|
return result;
|
|
}
|
|
|
|
var mapData = owner.MapData!;
|
|
var buildings = mapData.GetBuildings();
|
|
var pathways = mapData.GetPathways();
|
|
var areas = mapData.GetAreas();
|
|
var pois = mapData.GetPOIs();
|
|
var issues = new List<string>();
|
|
var stats = new List<string>();
|
|
|
|
// Check buildings have valid outlines
|
|
int validBuildings = 0;
|
|
foreach (var building in buildings)
|
|
{
|
|
if (building.Outline == null || building.Outline.Count < 3)
|
|
{
|
|
issues.Add($"Building {building.Id}: Invalid outline (< 3 points)");
|
|
}
|
|
else
|
|
{
|
|
validBuildings++;
|
|
}
|
|
}
|
|
stats.Add($"Valid buildings: {validBuildings}/{buildings.Count}");
|
|
|
|
// Check pathways have valid points
|
|
int validPathways = 0;
|
|
foreach (var pathway in pathways)
|
|
{
|
|
if (pathway.Points == null || pathway.Points.Count < 2)
|
|
{
|
|
issues.Add($"Pathway {pathway.Id}: Invalid points (< 2)");
|
|
}
|
|
else
|
|
{
|
|
validPathways++;
|
|
}
|
|
}
|
|
stats.Add($"Valid pathways: {validPathways}/{pathways.Count}");
|
|
|
|
// Check areas
|
|
int validAreas = 0;
|
|
foreach (var area in areas)
|
|
{
|
|
if (area.Outline == null || area.Outline.Count < 3)
|
|
{
|
|
issues.Add($"Area {area.Id}: Invalid outline");
|
|
}
|
|
else
|
|
{
|
|
validAreas++;
|
|
}
|
|
}
|
|
stats.Add($"Valid areas: {validAreas}/{areas.Count}");
|
|
|
|
// Check POIs have valid positions
|
|
int validPOIs = 0;
|
|
foreach (var poi in pois)
|
|
{
|
|
if (poi.Location.Lat != 0 || poi.Location.Lon != 0)
|
|
{
|
|
validPOIs++;
|
|
}
|
|
}
|
|
stats.Add($"Valid POIs: {validPOIs}/{pois.Count}");
|
|
|
|
result.Passed = issues.Count == 0;
|
|
result.Details = string.Join("\n", stats);
|
|
if (issues.Count > 0)
|
|
{
|
|
result.Error = string.Join("; ", issues.Take(5));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Error = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
owner?.Dispose();
|
|
player?.Dispose();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task<TestResult> TestTaskReachabilityAsync(Position center, double radius)
|
|
{
|
|
var result = new TestResult { TestName = "TaskReachability" };
|
|
SimulatorClient? owner = null;
|
|
SimulatorClient? player = null;
|
|
|
|
try
|
|
{
|
|
// Create lobby owner
|
|
owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "TaskOwner");
|
|
player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "TaskPlayer");
|
|
|
|
if (!await owner.ConnectAsync(_serverHost, _serverPort))
|
|
{
|
|
result.Error = "Owner failed to connect";
|
|
return result;
|
|
}
|
|
|
|
if (!await owner.CreateLobbyAsync(null, center, radius, 1, 5))
|
|
{
|
|
result.Error = "Failed to create lobby";
|
|
return result;
|
|
}
|
|
|
|
var joinCode = owner.JoinCode;
|
|
|
|
if (!await player.ConnectAsync(_serverHost, _serverPort))
|
|
{
|
|
result.Error = "Player failed to connect";
|
|
return result;
|
|
}
|
|
|
|
if (!await player.JoinLobbyAsync(joinCode!))
|
|
{
|
|
result.Error = "Player failed to join";
|
|
return result;
|
|
}
|
|
|
|
// Start game - this triggers map data fetch
|
|
Log(" Starting game (triggers Overpass API fetch)...");
|
|
if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
|
|
{
|
|
result.Error = "Timeout waiting for game to start";
|
|
return result;
|
|
}
|
|
|
|
// Check task positions
|
|
var mapData = owner.MapData;
|
|
var tasks = owner.MyTasks.Count > 0 ? owner.MyTasks : player.MyTasks;
|
|
|
|
if (tasks.Count == 0)
|
|
{
|
|
result.Error = "No tasks were assigned";
|
|
return result;
|
|
}
|
|
|
|
int tasksInPlayArea = 0;
|
|
int tasksOnPathways = 0;
|
|
var details = new List<string>();
|
|
|
|
foreach (var task in tasks)
|
|
{
|
|
var distFromCenter = task.Location.DistanceTo(center);
|
|
bool inPlayArea = distFromCenter <= radius;
|
|
if (inPlayArea) tasksInPlayArea++;
|
|
|
|
// Check if task is near any pathway
|
|
bool nearPathway = false;
|
|
if (mapData != null)
|
|
{
|
|
var pathways = mapData.GetPathways();
|
|
foreach (var pathway in pathways)
|
|
{
|
|
foreach (var point in pathway.Points)
|
|
{
|
|
if (task.Location.DistanceTo(point) < 15) // Within 15m of pathway
|
|
{
|
|
nearPathway = true;
|
|
break;
|
|
}
|
|
}
|
|
if (nearPathway) break;
|
|
}
|
|
}
|
|
if (nearPathway) tasksOnPathways++;
|
|
|
|
details.Add($"{task.Name}: {distFromCenter:F0}m from center, {(inPlayArea ? "in" : "OUT OF")} play area, {(nearPathway ? "near" : "NOT NEAR")} pathway");
|
|
}
|
|
|
|
result.Passed = tasksInPlayArea == tasks.Count;
|
|
result.Details = $"Tasks in play area: {tasksInPlayArea}/{tasks.Count}\n" +
|
|
$"Tasks near pathways: {tasksOnPathways}/{tasks.Count}\n" +
|
|
string.Join("\n", details.Take(3));
|
|
|
|
if (tasksInPlayArea < tasks.Count)
|
|
{
|
|
result.Error = $"{tasks.Count - tasksInPlayArea} tasks are outside play area!";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Error = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
owner?.Dispose();
|
|
player?.Dispose();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task<TestResult> TestRepairStationReachabilityAsync(Position center, double radius)
|
|
{
|
|
var result = new TestResult { TestName = "RepairStationReachability" };
|
|
SimulatorClient? owner = null;
|
|
SimulatorClient? player = null;
|
|
|
|
try
|
|
{
|
|
// Create lobby with 2 players
|
|
owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "SabOwner");
|
|
player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "SabPlayer");
|
|
|
|
if (!await owner.ConnectAsync(_serverHost, _serverPort)) { result.Error = "Owner connect failed"; return result; }
|
|
if (!await owner.CreateLobbyAsync(null, center, radius, 1, 3)) { result.Error = "Create lobby failed"; return result; }
|
|
|
|
var joinCode = owner.JoinCode;
|
|
|
|
if (!await player.ConnectAsync(_serverHost, _serverPort)) { result.Error = "Player connect failed"; return result; }
|
|
if (!await player.JoinLobbyAsync(joinCode!)) { result.Error = "Join lobby failed"; return result; }
|
|
|
|
// Start game - this triggers map data fetch
|
|
Log(" Starting game (triggers Overpass API fetch)...");
|
|
if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
|
|
{
|
|
result.Error = "Timeout waiting for game to start";
|
|
return result;
|
|
}
|
|
|
|
// Find impostor and trigger sabotage
|
|
var impostor = owner.IsImpostor ? owner : (player.IsImpostor ? player : null);
|
|
if (impostor == null)
|
|
{
|
|
result.Error = "No impostor found";
|
|
return result;
|
|
}
|
|
|
|
// Wait for sabotage cooldown
|
|
await Task.Delay(1000);
|
|
|
|
// Try to start a sabotage
|
|
impostor.TrySabotage(SabotageType.CriticalMeltdown);
|
|
await Task.Delay(1500);
|
|
|
|
// Check repair station positions
|
|
var crew = owner.IsCrew ? owner : player;
|
|
var stations = crew.RepairStations;
|
|
|
|
if (stations.Count == 0)
|
|
{
|
|
// Sabotage might have been blocked, try CommsBlackout
|
|
impostor.TrySabotage(SabotageType.CommsBlackout);
|
|
await Task.Delay(1000);
|
|
stations = crew.RepairStations;
|
|
}
|
|
|
|
if (stations.Count == 0)
|
|
{
|
|
result.Passed = true; // No sabotage was possible, but that's OK
|
|
result.Details = "Sabotage cooldown active, skipping repair station test";
|
|
return result;
|
|
}
|
|
|
|
int stationsInPlayArea = 0;
|
|
var details = new List<string>();
|
|
|
|
foreach (var station in stations)
|
|
{
|
|
var distFromCenter = station.Location.DistanceTo(center);
|
|
bool inPlayArea = distFromCenter <= radius;
|
|
if (inPlayArea) stationsInPlayArea++;
|
|
|
|
details.Add($"{station.Name}: {distFromCenter:F0}m from center ({(inPlayArea ? "OK" : "OUT!")})");
|
|
}
|
|
|
|
result.Passed = stationsInPlayArea == stations.Count;
|
|
result.Details = $"Stations in play area: {stationsInPlayArea}/{stations.Count}\n" +
|
|
string.Join("\n", details);
|
|
|
|
if (stationsInPlayArea < stations.Count)
|
|
{
|
|
result.Error = $"{stations.Count - stationsInPlayArea} repair stations are outside play area!";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Error = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
owner?.Dispose();
|
|
player?.Dispose();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task<TestResult> TestPlayAreaBoundaryAsync(Position center, double radius)
|
|
{
|
|
var result = new TestResult { TestName = "PlayAreaBoundary" };
|
|
SimulatorClient? owner = null;
|
|
SimulatorClient? player = null;
|
|
|
|
try
|
|
{
|
|
owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "BoundOwner");
|
|
player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "BoundPlayer");
|
|
|
|
if (!await owner.ConnectAsync(_serverHost, _serverPort)) { result.Error = "Connect failed"; return result; }
|
|
if (!await owner.CreateLobbyAsync(null, center, radius)) { result.Error = "Create lobby failed"; return result; }
|
|
|
|
if (!await player.ConnectAsync(_serverHost, _serverPort)) { result.Error = "Player connect failed"; return result; }
|
|
if (!await player.JoinLobbyAsync(owner.JoinCode!)) { result.Error = "Join lobby failed"; return result; }
|
|
|
|
// Start game - this triggers map data fetch
|
|
Log(" Starting game (triggers Overpass API fetch)...");
|
|
if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
|
|
{
|
|
result.Passed = true;
|
|
result.Details = "No map data to validate (Overpass might be disabled or timed out)";
|
|
return result;
|
|
}
|
|
|
|
if (!owner.HasMapData)
|
|
{
|
|
result.Passed = true;
|
|
result.Details = "No map data to validate (Overpass might be disabled)";
|
|
return result;
|
|
}
|
|
|
|
var mapData = owner.MapData!;
|
|
var pathways = mapData.GetPathways();
|
|
var pois = mapData.GetPOIs();
|
|
int outsidePathwayPoints = 0;
|
|
int totalPathwayPoints = 0;
|
|
int outsidePOIs = 0;
|
|
int totalPOIs = pois.Count;
|
|
|
|
// Check all pathway points
|
|
foreach (var pathway in pathways)
|
|
{
|
|
foreach (var point in pathway.Points)
|
|
{
|
|
totalPathwayPoints++;
|
|
if (point.DistanceTo(center) > radius * 1.1) // Allow 10% margin
|
|
{
|
|
outsidePathwayPoints++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check all POIs
|
|
foreach (var poi in pois)
|
|
{
|
|
if (poi.Location.DistanceTo(center) > radius * 1.1)
|
|
{
|
|
outsidePOIs++;
|
|
}
|
|
}
|
|
|
|
// Allow some points outside (Overpass query might include slightly outside data)
|
|
double pathwayOutsidePercent = totalPathwayPoints > 0 ? (outsidePathwayPoints * 100.0 / totalPathwayPoints) : 0;
|
|
double poiOutsidePercent = totalPOIs > 0 ? (outsidePOIs * 100.0 / totalPOIs) : 0;
|
|
|
|
result.Passed = pathwayOutsidePercent < 20 && poiOutsidePercent < 20;
|
|
result.Details = $"Pathway points outside boundary: {outsidePathwayPoints}/{totalPathwayPoints} ({pathwayOutsidePercent:F1}%)\n" +
|
|
$"POIs outside boundary: {outsidePOIs}/{totalPOIs} ({poiOutsidePercent:F1}%)";
|
|
|
|
if (!result.Passed)
|
|
{
|
|
result.Error = "Too many map elements outside play area boundary";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Error = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
owner?.Dispose();
|
|
player?.Dispose();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task<TestResult> TestMapDataConsistencyAsync(Position center, double radius)
|
|
{
|
|
var result = new TestResult { TestName = "MapDataConsistency" };
|
|
var owners = new List<SimulatorClient>();
|
|
var players = new List<SimulatorClient>();
|
|
|
|
try
|
|
{
|
|
// Create 3 lobbies at the same location and compare map data
|
|
var mapDataResults = new List<(int buildings, int pathways, int pois)>();
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
Log($" Creating lobby {i + 1}/3...");
|
|
var owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), $"ConsOwner{i}");
|
|
var player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), $"ConsPlayer{i}");
|
|
owners.Add(owner);
|
|
players.Add(player);
|
|
|
|
if (!await owner.ConnectAsync(_serverHost, _serverPort)) continue;
|
|
if (!await owner.CreateLobbyAsync(null, center, radius)) continue;
|
|
|
|
if (!await player.ConnectAsync(_serverHost, _serverPort)) continue;
|
|
if (!await player.JoinLobbyAsync(owner.JoinCode!)) continue;
|
|
|
|
// Start game to trigger map data fetch
|
|
if (await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
|
|
{
|
|
if (owner.HasMapData)
|
|
{
|
|
var md = owner.MapData!;
|
|
mapDataResults.Add((md.GetBuildings().Count, md.GetPathways().Count, md.GetPOIs().Count));
|
|
}
|
|
}
|
|
|
|
await Task.Delay(300);
|
|
}
|
|
|
|
if (mapDataResults.Count < 2)
|
|
{
|
|
result.Passed = true;
|
|
result.Details = "Not enough successful map data fetches to compare consistency";
|
|
return result;
|
|
}
|
|
|
|
// Check if all results are the same (should be cached)
|
|
var first = mapDataResults[0];
|
|
bool allSame = mapDataResults.All(r => r == first);
|
|
|
|
result.Passed = allSame;
|
|
result.Details = string.Join("\n", mapDataResults.Select((r, i) =>
|
|
$"Lobby {i + 1}: {r.buildings} buildings, {r.pathways} pathways, {r.pois} POIs"));
|
|
|
|
if (!allSame)
|
|
{
|
|
result.Error = "Map data varies between lobbies at same location (caching issue?)";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Error = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
foreach (var o in owners) o?.Dispose();
|
|
foreach (var p in players) p?.Dispose();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Results from a single test
|
|
/// </summary>
|
|
public class TestResult
|
|
{
|
|
public string TestName { get; set; } = "";
|
|
public bool Passed { get; set; }
|
|
public string? Details { get; set; }
|
|
public string? Error { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggregate results from all Overpass tests
|
|
/// </summary>
|
|
public class OverpassTestResults
|
|
{
|
|
public TestResult MapDataFetchTest { get; set; } = new();
|
|
public TestResult MapDataStructureTest { get; set; } = new();
|
|
public TestResult TaskReachabilityTest { get; set; } = new();
|
|
public TestResult RepairStationReachabilityTest { get; set; } = new();
|
|
public TestResult PlayAreaBoundaryTest { get; set; } = new();
|
|
public TestResult ConsistencyTest { get; set; } = new();
|
|
|
|
public int TotalTests => 6;
|
|
public int PassedTests => new[] { MapDataFetchTest, MapDataStructureTest, TaskReachabilityTest,
|
|
RepairStationReachabilityTest, PlayAreaBoundaryTest, ConsistencyTest }.Count(t => t.Passed);
|
|
public int FailedTests => TotalTests - PassedTests;
|
|
public bool AllPassed => PassedTests == TotalTests;
|
|
|
|
public void PrintSummary(Action<string> log)
|
|
{
|
|
log($" Total Tests: {TotalTests}");
|
|
log($" Passed: {PassedTests}");
|
|
log($" Failed: {FailedTests}");
|
|
log("");
|
|
|
|
if (AllPassed)
|
|
{
|
|
log(" ✓ ALL OVERPASS TESTS PASSED!");
|
|
}
|
|
else
|
|
{
|
|
log(" ✗ SOME TESTS FAILED:");
|
|
if (!MapDataFetchTest.Passed) log($" - Map Data Fetch: {MapDataFetchTest.Error}");
|
|
if (!MapDataStructureTest.Passed) log($" - Map Data Structure: {MapDataStructureTest.Error}");
|
|
if (!TaskReachabilityTest.Passed) log($" - Task Reachability: {TaskReachabilityTest.Error}");
|
|
if (!RepairStationReachabilityTest.Passed) log($" - Repair Station Reachability: {RepairStationReachabilityTest.Error}");
|
|
if (!PlayAreaBoundaryTest.Passed) log($" - Play Area Boundary: {PlayAreaBoundaryTest.Error}");
|
|
if (!ConsistencyTest.Passed) log($" - Consistency: {ConsistencyTest.Error}");
|
|
}
|
|
}
|
|
}
|
|
}
|