813 lines
30 KiB
C#
813 lines
30 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Logic;
|
|
using Logic.AI;
|
|
using Logic.CrashSight;
|
|
using RuntimeData;
|
|
using TH1_Core.Managers;
|
|
using TH1_Logic.Core;
|
|
using TH1_Logic.MatchConfig;
|
|
using TH1_Logic.Net;
|
|
using TH1Renderer;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
|
|
namespace TH1_Logic.Editor
|
|
{
|
|
[InitializeOnLoad]
|
|
public static class AIDirectorBatchRunner
|
|
{
|
|
private const string PendingKey = "TH1.AIDirectorBatch.Pending";
|
|
private const string OptionsKey = "TH1.AIDirectorBatch.Options";
|
|
private const string GameIndexKey = "TH1.AIDirectorBatch.GameIndex";
|
|
private const string ResultsKey = "TH1.AIDirectorBatch.Results";
|
|
|
|
static AIDirectorBatchRunner()
|
|
{
|
|
if (SessionState.GetBool(PendingKey, false))
|
|
{
|
|
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
|
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
|
}
|
|
}
|
|
|
|
[MenuItem("Tools/TH1/AI Director/Run Batch")]
|
|
public static void RunFromMenu()
|
|
{
|
|
StartBatch(ParseOptionsFromCommandLine());
|
|
}
|
|
|
|
public static void Run()
|
|
{
|
|
StartBatch(ParseOptionsFromCommandLine());
|
|
}
|
|
|
|
private static void StartBatch(BatchOptions options)
|
|
{
|
|
try
|
|
{
|
|
options.Normalize();
|
|
Directory.CreateDirectory(options.OutputDirectory);
|
|
SessionState.SetString(OptionsKey, JsonUtility.ToJson(options));
|
|
SessionState.SetInt(GameIndexKey, 0);
|
|
SessionState.SetString(ResultsKey, JsonUtility.ToJson(new BatchResultList()));
|
|
SessionState.SetBool(PendingKey, true);
|
|
|
|
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
|
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
|
|
|
if (!EditorApplication.isPlaying)
|
|
{
|
|
OpenStartupScene(options.ScenePath);
|
|
EditorApplication.EnterPlaymode();
|
|
return;
|
|
}
|
|
|
|
StartPlayModeRunner();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"[AI.Batch] Start failed:\n{e}");
|
|
ExitEditor(1);
|
|
}
|
|
}
|
|
|
|
private static void OnPlayModeStateChanged(PlayModeStateChange state)
|
|
{
|
|
if (!SessionState.GetBool(PendingKey, false)) return;
|
|
if (state != PlayModeStateChange.EnteredPlayMode) return;
|
|
|
|
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
|
StartPlayModeRunner();
|
|
}
|
|
|
|
private static void StartPlayModeRunner()
|
|
{
|
|
EditorApplication.update -= WaitForMainAndStartCoroutine;
|
|
EditorApplication.update += WaitForMainAndStartCoroutine;
|
|
}
|
|
|
|
private static void WaitForMainAndStartCoroutine()
|
|
{
|
|
if (!EditorApplication.isPlaying) return;
|
|
if (Main.Instance == null) return;
|
|
|
|
EditorApplication.update -= WaitForMainAndStartCoroutine;
|
|
Main.Instance.StartCoroutine(RunChecked(RunBatchCoroutine(), HandleFatalBatchException));
|
|
}
|
|
|
|
private static IEnumerator RunBatchCoroutine()
|
|
{
|
|
var options = ReadOptions();
|
|
var gameIndex = SessionState.GetInt(GameIndexKey, 0);
|
|
var results = ReadResults();
|
|
|
|
while (gameIndex < options.Games)
|
|
{
|
|
var result = new BatchGameResult
|
|
{
|
|
gameIndex = gameIndex,
|
|
startedAt = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
|
|
};
|
|
|
|
var gameDirectory = Path.Combine(options.OutputDirectory, $"game_{gameIndex + 1:000}");
|
|
Directory.CreateDirectory(gameDirectory);
|
|
|
|
Exception gameException = null;
|
|
yield return RunChecked(RunGameCoroutine(options, result, gameIndex), e => gameException = e);
|
|
|
|
if (gameException != null)
|
|
{
|
|
result.success = false;
|
|
result.reason = $"Exception: {gameException.GetType().Name}: {gameException.Message}";
|
|
result.stackTrace = gameException.ToString();
|
|
Debug.LogError($"[AI.Batch] Game {gameIndex + 1} failed:\n{gameException}");
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
AIDirectorBatchRuntime.ForceAllPlayersAi = false;
|
|
AIDirectorBatchRuntime.SkipPresentationWait = false;
|
|
#endif
|
|
CompleteResult(result, gameDirectory);
|
|
results.Add(result);
|
|
WriteGameSummary(gameDirectory, result);
|
|
WriteBatchSummary(options, results);
|
|
SessionState.SetString(ResultsKey, JsonUtility.ToJson(new BatchResultList { results = results }));
|
|
gameIndex++;
|
|
SessionState.SetInt(GameIndexKey, gameIndex);
|
|
|
|
if (!result.success && options.FailFast) break;
|
|
if (gameIndex < options.Games)
|
|
{
|
|
yield return ResetForNextGame();
|
|
}
|
|
}
|
|
|
|
var hasFailure = results.Any(item => !item.success);
|
|
CleanupSessionState();
|
|
Debug.Log($"[AI.Batch] Finished. games={results.Count}, failed={results.Count(item => !item.success)}, output={options.OutputDirectory}");
|
|
ExitEditor(hasFailure ? 1 : 0);
|
|
}
|
|
|
|
private static IEnumerator RunGameCoroutine(BatchOptions options, BatchGameResult result, int gameIndex)
|
|
{
|
|
yield return WaitForRuntimeReady(options);
|
|
ConfigureDebugRuntime(options);
|
|
AILogic.UseDirectorKernel();
|
|
AIDirectorBatchRuntime.ForceAllPlayersAi = true;
|
|
AIDirectorBatchRuntime.SkipPresentationWait = true;
|
|
|
|
var main = Main.Instance;
|
|
main.MapConfig = BuildMapConfig(options, gameIndex);
|
|
result.playerCount = (int)main.MapConfig.PlayerCount;
|
|
result.width = (int)main.MapConfig.Width;
|
|
result.height = (int)main.MapConfig.Height;
|
|
result.aiKernel = AIKernelRegistry.CurrentKernelType.ToString();
|
|
|
|
Debug.Log($"[AI.Batch] Start game {gameIndex + 1}/{options.Games}: players={result.playerCount}, size={result.width}x{result.height}");
|
|
main.StartMatch();
|
|
|
|
yield return null;
|
|
yield return RunOneGame(options, result);
|
|
}
|
|
|
|
private static IEnumerator RunChecked(IEnumerator root, Action<Exception> onException)
|
|
{
|
|
var stack = new Stack<IEnumerator>();
|
|
stack.Push(root);
|
|
|
|
while (stack.Count > 0)
|
|
{
|
|
object current;
|
|
try
|
|
{
|
|
var top = stack.Peek();
|
|
if (!top.MoveNext())
|
|
{
|
|
stack.Pop();
|
|
continue;
|
|
}
|
|
current = top.Current;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
onException?.Invoke(e);
|
|
yield break;
|
|
}
|
|
|
|
if (current is IEnumerator nested)
|
|
{
|
|
stack.Push(nested);
|
|
continue;
|
|
}
|
|
|
|
yield return current;
|
|
}
|
|
}
|
|
|
|
private static void HandleFatalBatchException(Exception exception)
|
|
{
|
|
Debug.LogError($"[AI.Batch] Fatal failure:\n{exception}");
|
|
#if UNITY_EDITOR
|
|
AIDirectorBatchRuntime.ForceAllPlayersAi = false;
|
|
AIDirectorBatchRuntime.SkipPresentationWait = false;
|
|
#endif
|
|
try
|
|
{
|
|
var options = ReadOptionsSafe();
|
|
var results = ReadResultsSafe();
|
|
var gameIndex = SessionState.GetInt(GameIndexKey, 0);
|
|
var gameDirectory = Path.Combine(options.OutputDirectory, $"game_{gameIndex + 1:000}");
|
|
Directory.CreateDirectory(gameDirectory);
|
|
|
|
var result = new BatchGameResult
|
|
{
|
|
gameIndex = gameIndex,
|
|
success = false,
|
|
reason = $"FatalException:{exception.GetType().Name}:{exception.Message}",
|
|
stackTrace = exception.ToString(),
|
|
startedAt = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
|
|
};
|
|
|
|
CompleteResult(result, gameDirectory);
|
|
results.Add(result);
|
|
WriteGameSummary(gameDirectory, result);
|
|
WriteBatchSummary(options, results);
|
|
}
|
|
catch (Exception writeException)
|
|
{
|
|
Debug.LogError($"[AI.Batch] Failed to write fatal summary:\n{writeException}");
|
|
}
|
|
finally
|
|
{
|
|
CleanupSessionState();
|
|
ExitEditor(1);
|
|
}
|
|
}
|
|
|
|
private static IEnumerator WaitForRuntimeReady(BatchOptions options)
|
|
{
|
|
var deadline = EditorApplication.timeSinceStartup + options.StartupTimeoutSeconds;
|
|
while (EditorApplication.timeSinceStartup <= deadline)
|
|
{
|
|
var main = Main.Instance;
|
|
if (main != null
|
|
&& main.GameLogic != null
|
|
&& Main.PlayerLogic != null
|
|
&& Main.CityLogic != null
|
|
&& Main.UnitLogic != null
|
|
&& UIManager.Instance != null)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
throw new TimeoutException($"Runtime startup timeout after {options.StartupTimeoutSeconds} seconds.");
|
|
}
|
|
|
|
private static IEnumerator ResetForNextGame()
|
|
{
|
|
var main = Main.Instance;
|
|
if (main != null)
|
|
{
|
|
main.Clear();
|
|
yield return null;
|
|
while (PresentationManager.Busy) yield return null;
|
|
}
|
|
}
|
|
|
|
private static IEnumerator RunOneGame(BatchOptions options, BatchGameResult result)
|
|
{
|
|
var deadline = EditorApplication.timeSinceStartup + options.TimeoutSeconds;
|
|
var lastNetActionCount = GetNetActionCount();
|
|
var lastTurnKey = string.Empty;
|
|
var actionsThisPlayerTurn = 0;
|
|
var stagnantFrames = 0;
|
|
|
|
while (EditorApplication.timeSinceStartup <= deadline)
|
|
{
|
|
yield return null;
|
|
result.frames++;
|
|
|
|
var map = Main.MapData;
|
|
if (map == null)
|
|
{
|
|
result.success = false;
|
|
result.reason = "Main.MapData is null.";
|
|
yield break;
|
|
}
|
|
|
|
result.netActions = GetNetActionCount();
|
|
result.maxPlayerTurn = GetMaxPlayerTurn(map);
|
|
result.curPlayerId = map.CurPlayer?.Id ?? 0;
|
|
result.curPlayerTurn = map.CurPlayer?.Turn ?? 0;
|
|
result.survivingPlayers = CountSurvivingPlayers(map);
|
|
result.gameState = Main.Instance.GameLogic?.GetCurState().ToString() ?? string.Empty;
|
|
|
|
if (map.CheckIfGameEnd(out var isWin)
|
|
|| Main.Instance.GameLogic?.GetCurState() == GameState.Finished)
|
|
{
|
|
result.success = true;
|
|
result.reason = isWin ? "GameEnd:SelfOrTeamWin" : "GameEnd";
|
|
yield break;
|
|
}
|
|
|
|
if (options.MaxTurns > 0 && result.maxPlayerTurn >= options.MaxTurns)
|
|
{
|
|
result.success = true;
|
|
result.reason = $"ReachedMaxTurns:{options.MaxTurns}";
|
|
yield break;
|
|
}
|
|
|
|
if (options.MaxActions > 0 && result.netActions >= options.MaxActions)
|
|
{
|
|
result.success = false;
|
|
result.reason = $"ReachedMaxActions:{options.MaxActions}";
|
|
yield break;
|
|
}
|
|
|
|
var turnKey = $"{result.curPlayerId}:{result.curPlayerTurn}";
|
|
if (turnKey != lastTurnKey)
|
|
{
|
|
lastTurnKey = turnKey;
|
|
actionsThisPlayerTurn = 0;
|
|
}
|
|
|
|
if (result.netActions > lastNetActionCount)
|
|
{
|
|
actionsThisPlayerTurn += result.netActions - lastNetActionCount;
|
|
lastNetActionCount = result.netActions;
|
|
stagnantFrames = 0;
|
|
}
|
|
else
|
|
{
|
|
stagnantFrames++;
|
|
}
|
|
|
|
if (options.MaxActionsPerPlayerTurn > 0 && actionsThisPlayerTurn > options.MaxActionsPerPlayerTurn)
|
|
{
|
|
result.success = false;
|
|
result.reason = $"ActionsPerPlayerTurnGuard:{actionsThisPlayerTurn}>{options.MaxActionsPerPlayerTurn}";
|
|
yield break;
|
|
}
|
|
|
|
if (options.StagnantFrameLimit > 0 && stagnantFrames > options.StagnantFrameLimit)
|
|
{
|
|
result.success = false;
|
|
result.reason = $"StagnantFrameGuard:{stagnantFrames}";
|
|
yield break;
|
|
}
|
|
}
|
|
|
|
result.success = false;
|
|
result.reason = $"Timeout:{options.TimeoutSeconds}s";
|
|
}
|
|
|
|
private static MapConfig BuildMapConfig(BatchOptions options, int gameIndex)
|
|
{
|
|
var config = new MapConfig((uint)options.Width, (uint)options.Height, (uint)options.Players, 0, 0, options.Difficulty)
|
|
{
|
|
GameMode = GameMode.DOMINATION,
|
|
MatchSettlement = MatchSettlementType.Normal,
|
|
IsLimitTime = false,
|
|
DisableNearbySpawnPoints = options.DisableNearbySpawnPoints,
|
|
WaterType = options.WaterType
|
|
};
|
|
|
|
config.SetPlayerCount((uint)options.Players, NetMode.Single);
|
|
var used = new HashSet<uint>();
|
|
for (var i = 0; i < options.Players; i++)
|
|
{
|
|
var civId = PickCivId(i + gameIndex, used);
|
|
used.Add(civId);
|
|
if (!config.SetSinglePlayerSlotCiv(i, civId, civId))
|
|
{
|
|
config.SetPlayerSlotRandomCiv(i, NetMode.Single);
|
|
}
|
|
}
|
|
|
|
config.EnsurePlayerSlots(NetMode.Single);
|
|
return config;
|
|
}
|
|
|
|
private static uint PickCivId(int preferredIndex, HashSet<uint> used)
|
|
{
|
|
const int defaultCivCount = 17;
|
|
uint fallback = 0;
|
|
var hasFallback = false;
|
|
for (var offset = 0; offset < defaultCivCount; offset++)
|
|
{
|
|
var civId = (uint)((preferredIndex + offset) % defaultCivCount);
|
|
if (!ContentGate.CanUseEmpire(civId, civId)) continue;
|
|
if (!hasFallback)
|
|
{
|
|
fallback = civId;
|
|
hasFallback = true;
|
|
}
|
|
if (!used.Contains(civId)) return civId;
|
|
}
|
|
|
|
return hasFallback ? fallback : 0;
|
|
}
|
|
|
|
private static void ConfigureDebugRuntime(BatchOptions options)
|
|
{
|
|
var main = Main.Instance;
|
|
if (main != null)
|
|
{
|
|
main.NoAI = false;
|
|
main.FullSight = true;
|
|
main.AIActionTime = 0f;
|
|
main.AnimationSpeed = Mathf.Max(1f, options.AnimationSpeed);
|
|
main.DebugMode = true;
|
|
main.DebugHideCenterMessage = true;
|
|
}
|
|
|
|
if (DebugCenter.Instance == null) return;
|
|
DebugCenter.Instance.DebugNoAI = false;
|
|
DebugCenter.Instance.DebugSelfPlayerAllSight = true;
|
|
DebugCenter.Instance.DebugAIActionTime = 0f;
|
|
DebugCenter.Instance.AnimationSpeed = Mathf.Max(1f, options.AnimationSpeed);
|
|
DebugCenter.Instance.DebugMode = true;
|
|
DebugCenter.Instance.DebugHideCenterMessage = true;
|
|
}
|
|
|
|
private static BatchOptions ParseOptionsFromCommandLine()
|
|
{
|
|
var options = new BatchOptions();
|
|
options.Games = GetIntArg("-aiBatchGames", options.Games);
|
|
options.Players = GetIntArg("-aiBatchPlayers", options.Players);
|
|
options.Width = GetIntArg("-aiBatchWidth", options.Width);
|
|
options.Height = GetIntArg("-aiBatchHeight", options.Height);
|
|
options.MaxTurns = GetIntArg("-aiBatchTurns", options.MaxTurns);
|
|
options.TimeoutSeconds = GetIntArg("-aiBatchTimeoutSeconds", options.TimeoutSeconds);
|
|
options.StartupTimeoutSeconds = GetIntArg("-aiBatchStartupTimeoutSeconds", options.StartupTimeoutSeconds);
|
|
options.MaxActions = GetIntArg("-aiBatchMaxActions", options.MaxActions);
|
|
options.MaxActionsPerPlayerTurn = GetIntArg("-aiBatchMaxActionsPerPlayerTurn", options.MaxActionsPerPlayerTurn);
|
|
options.StagnantFrameLimit = GetIntArg("-aiBatchStagnantFrames", options.StagnantFrameLimit);
|
|
options.AnimationSpeed = GetFloatArg("-aiBatchAnimationSpeed", options.AnimationSpeed);
|
|
options.FailFast = GetBoolArg("-aiBatchFailFast", options.FailFast);
|
|
options.DisableNearbySpawnPoints = GetBoolArg("-aiBatchDisableNearbySpawnPoints", options.DisableNearbySpawnPoints);
|
|
|
|
var outputDirectory = GetStringArg("-aiBatchOut");
|
|
if (!string.IsNullOrWhiteSpace(outputDirectory)) options.OutputDirectory = outputDirectory;
|
|
|
|
var scenePath = GetStringArg("-aiBatchScene");
|
|
if (!string.IsNullOrWhiteSpace(scenePath)) options.ScenePath = scenePath;
|
|
|
|
var difficulty = GetStringArg("-aiBatchDifficulty");
|
|
if (!string.IsNullOrWhiteSpace(difficulty) && Enum.TryParse(difficulty, true, out AIDifficult parsedDifficulty))
|
|
options.Difficulty = parsedDifficulty;
|
|
|
|
var waterType = GetStringArg("-aiBatchWaterType");
|
|
if (!string.IsNullOrWhiteSpace(waterType) && Enum.TryParse(waterType, true, out MapWaterType parsedWaterType))
|
|
options.WaterType = parsedWaterType;
|
|
|
|
return options;
|
|
}
|
|
|
|
private static BatchOptions ReadOptions()
|
|
{
|
|
var json = SessionState.GetString(OptionsKey, string.Empty);
|
|
var options = string.IsNullOrWhiteSpace(json)
|
|
? new BatchOptions()
|
|
: JsonUtility.FromJson<BatchOptions>(json);
|
|
options.Normalize();
|
|
return options;
|
|
}
|
|
|
|
private static List<BatchGameResult> ReadResults()
|
|
{
|
|
var json = SessionState.GetString(ResultsKey, string.Empty);
|
|
if (string.IsNullOrWhiteSpace(json)) return new List<BatchGameResult>();
|
|
if (json.TrimStart().StartsWith("[", StringComparison.Ordinal)) return new List<BatchGameResult>();
|
|
var list = JsonUtility.FromJson<BatchResultList>(json);
|
|
return list?.results ?? new List<BatchGameResult>();
|
|
}
|
|
|
|
private static BatchOptions ReadOptionsSafe()
|
|
{
|
|
try
|
|
{
|
|
return ReadOptions();
|
|
}
|
|
catch
|
|
{
|
|
var options = ParseOptionsFromCommandLine();
|
|
options.Normalize();
|
|
return options;
|
|
}
|
|
}
|
|
|
|
private static List<BatchGameResult> ReadResultsSafe()
|
|
{
|
|
try
|
|
{
|
|
return ReadResults();
|
|
}
|
|
catch
|
|
{
|
|
return new List<BatchGameResult>();
|
|
}
|
|
}
|
|
|
|
private static void CompleteResult(BatchGameResult result, string gameDirectory)
|
|
{
|
|
result.endedAt = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture);
|
|
result.summaryPath = Path.Combine(gameDirectory, "batch_game_summary.json").Replace('\\', '/');
|
|
var map = Main.MapData;
|
|
if (map == null) return;
|
|
|
|
result.mapId = map.MapID;
|
|
result.netActions = GetNetActionCount();
|
|
result.maxPlayerTurn = GetMaxPlayerTurn(map);
|
|
result.curPlayerId = map.CurPlayer?.Id ?? 0;
|
|
result.curPlayerTurn = map.CurPlayer?.Turn ?? 0;
|
|
result.survivingPlayers = CountSurvivingPlayers(map);
|
|
result.players = BuildPlayerResults(map);
|
|
}
|
|
|
|
private static List<BatchPlayerResult> BuildPlayerResults(MapData map)
|
|
{
|
|
var results = new List<BatchPlayerResult>();
|
|
var players = map.PlayerMap?.PlayerDataList;
|
|
if (players == null) return results;
|
|
|
|
foreach (var player in players)
|
|
{
|
|
if (player == null) continue;
|
|
results.Add(new BatchPlayerResult
|
|
{
|
|
id = player.Id,
|
|
civId = player.PlayerCivId,
|
|
forceId = player.PlayerForceId,
|
|
turn = player.Turn,
|
|
alive = player.Alive,
|
|
score = player.PlayerScore,
|
|
coin = player.PlayerCoin,
|
|
techPoint = player.PlayerTechPoint,
|
|
cityCount = CountPlayerCities(map, player.Id),
|
|
unitCount = CountPlayerUnits(map, player.Id),
|
|
isWin = map.MatchSettlement?.IsWin(player.Id) ?? false
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static int CountPlayerCities(MapData map, uint playerId)
|
|
{
|
|
var cities = map.CityMap?.CityList;
|
|
if (cities == null) return 0;
|
|
var count = 0;
|
|
foreach (var city in cities)
|
|
{
|
|
if (city != null && map.CheckCityIdBelongPlayerId(city.Id, playerId)) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
private static int CountPlayerUnits(MapData map, uint playerId)
|
|
{
|
|
var units = map.UnitMap?.UnitList;
|
|
if (units == null) return 0;
|
|
var count = 0;
|
|
foreach (var unit in units)
|
|
{
|
|
if (unit != null && map.GetPlayerIdByUnitId(unit.Id, out var ownerId) && ownerId == playerId && unit.IsAlive()) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
private static int CountSurvivingPlayers(MapData map)
|
|
{
|
|
var players = map.PlayerMap?.PlayerDataList;
|
|
if (players == null) return 0;
|
|
var count = 0;
|
|
foreach (var player in players)
|
|
{
|
|
if (player != null && player.IsSurvival) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
private static uint GetMaxPlayerTurn(MapData map)
|
|
{
|
|
var players = map.PlayerMap?.PlayerDataList;
|
|
if (players == null || players.Count == 0) return 0;
|
|
var maxTurn = 0u;
|
|
foreach (var player in players)
|
|
{
|
|
if (player != null && player.Turn > maxTurn) maxTurn = player.Turn;
|
|
}
|
|
return maxTurn;
|
|
}
|
|
|
|
private static int GetNetActionCount()
|
|
{
|
|
return Main.MapData?.Net?.Actions?.Count ?? 0;
|
|
}
|
|
|
|
private static void WriteGameSummary(string gameDirectory, BatchGameResult result)
|
|
{
|
|
var path = Path.Combine(gameDirectory, "batch_game_summary.json");
|
|
File.WriteAllText(path, JsonUtility.ToJson(result, true), Encoding.UTF8);
|
|
}
|
|
|
|
private static void WriteBatchSummary(BatchOptions options, List<BatchGameResult> results)
|
|
{
|
|
var summary = new BatchSummary
|
|
{
|
|
options = options,
|
|
results = results,
|
|
generatedAt = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture),
|
|
totalGames = results.Count,
|
|
failedGames = results.Count(item => !item.success)
|
|
};
|
|
var path = Path.Combine(options.OutputDirectory, "batch_summary.json");
|
|
File.WriteAllText(path, JsonUtility.ToJson(summary, true), Encoding.UTF8);
|
|
}
|
|
|
|
private static void OpenStartupScene(string scenePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(scenePath))
|
|
{
|
|
var scene = EditorBuildSettings.scenes.FirstOrDefault(item => item.enabled);
|
|
scenePath = scene?.path;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(scenePath)) return;
|
|
if (!File.Exists(Path.Combine(ProjectRoot, scenePath)))
|
|
{
|
|
throw new FileNotFoundException($"Startup scene not found: {scenePath}");
|
|
}
|
|
|
|
EditorSceneManager.OpenScene(scenePath);
|
|
}
|
|
|
|
private static string GetStringArg(string name)
|
|
{
|
|
var args = Environment.GetCommandLineArgs();
|
|
for (var i = 0; i < args.Length - 1; i++)
|
|
{
|
|
if (!string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase)) continue;
|
|
return args[i + 1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static int GetIntArg(string name, int fallback)
|
|
{
|
|
var value = GetStringArg(name);
|
|
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;
|
|
}
|
|
|
|
private static float GetFloatArg(string name, float fallback)
|
|
{
|
|
var value = GetStringArg(name);
|
|
return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;
|
|
}
|
|
|
|
private static bool GetBoolArg(string name, bool fallback)
|
|
{
|
|
var value = GetStringArg(name);
|
|
if (string.IsNullOrWhiteSpace(value)) return fallback;
|
|
if (bool.TryParse(value, out var parsed)) return parsed;
|
|
return value == "1" || value.Equals("yes", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static void CleanupSessionState()
|
|
{
|
|
SessionState.EraseBool(PendingKey);
|
|
SessionState.EraseString(OptionsKey);
|
|
SessionState.EraseInt(GameIndexKey);
|
|
SessionState.EraseString(ResultsKey);
|
|
}
|
|
|
|
private static void ExitEditor(int code)
|
|
{
|
|
if (Application.isBatchMode)
|
|
{
|
|
EditorApplication.Exit(code);
|
|
return;
|
|
}
|
|
|
|
if (EditorApplication.isPlaying)
|
|
{
|
|
EditorApplication.ExitPlaymode();
|
|
}
|
|
}
|
|
|
|
private static string ProjectRoot => Directory.GetParent(Application.dataPath).FullName;
|
|
}
|
|
|
|
[Serializable]
|
|
public class BatchOptions
|
|
{
|
|
public int Games = 1;
|
|
public int Players = 17;
|
|
public int Width = 30;
|
|
public int Height = 30;
|
|
public int MaxTurns = 100;
|
|
public int TimeoutSeconds = 1800;
|
|
public int StartupTimeoutSeconds = 180;
|
|
public int MaxActions = 20000;
|
|
public int MaxActionsPerPlayerTurn = 260;
|
|
public int StagnantFrameLimit = 3600;
|
|
public float AnimationSpeed = 100f;
|
|
public bool FailFast = true;
|
|
public bool DisableNearbySpawnPoints = false;
|
|
public string OutputDirectory;
|
|
public string ScenePath;
|
|
public AIDifficult Difficulty = AIDifficult.LUNATIC;
|
|
public MapWaterType WaterType = MapWaterType.Pangea;
|
|
|
|
public void Normalize()
|
|
{
|
|
Games = Mathf.Clamp(Games, 1, 1000);
|
|
Players = Mathf.Clamp(Players, 2, 17);
|
|
Width = Mathf.Clamp(Width, 8, 80);
|
|
Height = Mathf.Clamp(Height, 8, 80);
|
|
MaxTurns = Mathf.Max(0, MaxTurns);
|
|
TimeoutSeconds = Mathf.Max(30, TimeoutSeconds);
|
|
StartupTimeoutSeconds = Mathf.Max(30, StartupTimeoutSeconds);
|
|
MaxActions = Mathf.Max(0, MaxActions);
|
|
MaxActionsPerPlayerTurn = Mathf.Max(0, MaxActionsPerPlayerTurn);
|
|
StagnantFrameLimit = Mathf.Max(0, StagnantFrameLimit);
|
|
AnimationSpeed = Mathf.Max(1f, AnimationSpeed);
|
|
if (string.IsNullOrWhiteSpace(OutputDirectory))
|
|
{
|
|
var root = Directory.GetParent(Application.dataPath).FullName;
|
|
OutputDirectory = Path.Combine(root, "Logs", "AI_Batch", DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture));
|
|
}
|
|
OutputDirectory = Path.GetFullPath(OutputDirectory);
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class BatchSummary
|
|
{
|
|
public string generatedAt;
|
|
public int totalGames;
|
|
public int failedGames;
|
|
public BatchOptions options;
|
|
public List<BatchGameResult> results;
|
|
}
|
|
|
|
[Serializable]
|
|
public class BatchResultList
|
|
{
|
|
public List<BatchGameResult> results = new();
|
|
}
|
|
|
|
[Serializable]
|
|
public class BatchGameResult
|
|
{
|
|
public int gameIndex;
|
|
public bool success;
|
|
public string reason;
|
|
public string startedAt;
|
|
public string endedAt;
|
|
public string summaryPath;
|
|
public string stackTrace;
|
|
public string aiKernel;
|
|
public string gameState;
|
|
public uint mapId;
|
|
public int playerCount;
|
|
public int width;
|
|
public int height;
|
|
public int frames;
|
|
public int netActions;
|
|
public uint maxPlayerTurn;
|
|
public uint curPlayerId;
|
|
public uint curPlayerTurn;
|
|
public int survivingPlayers;
|
|
public List<BatchPlayerResult> players = new();
|
|
}
|
|
|
|
[Serializable]
|
|
public class BatchPlayerResult
|
|
{
|
|
public uint id;
|
|
public uint civId;
|
|
public uint forceId;
|
|
public uint turn;
|
|
public bool alive;
|
|
public bool isWin;
|
|
public int score;
|
|
public int coin;
|
|
public int techPoint;
|
|
public int cityCount;
|
|
public int unitCount;
|
|
}
|
|
}
|