feat: implement SimulationController, fix API paths, wire Simulator scene
This commit is contained in:
1 parent
d8e355de2b
commit
ce80b6d9c5
4 files changed
+756
-53
No files matched your search
@@ -14,11 +14,13 @@ public static class CreateSimulatorScene
|
||||
{
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────
|
||||
var services = new GameObject("[Services]");
|
||||
services.AddComponent<CommanderAILab.Services.ApiClient>();
|
||||
|
||||
// ── Canvas ────────────────────────────────────────────────────────────
|
||||
var canvasGo = new GameObject("Canvas");
|
||||
var canvas = canvasGo.AddComponent<Canvas>();
|
||||
var canvas = canvasGo.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
var scaler = canvasGo.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
@@ -32,56 +34,166 @@ public static class CreateSimulatorScene
|
||||
es.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
|
||||
}
|
||||
|
||||
// ── Background ────────────────────────────────────────────────────────
|
||||
CreatePanel(canvasGo.transform, "Background", new Color(0.08f, 0.08f, 0.12f), stretch: true);
|
||||
|
||||
// Header
|
||||
var header = CreatePanel(canvasGo.transform, "Header", new Color(0.12f, 0.12f, 0.18f));
|
||||
// ── Header (Top Bar) ──────────────────────────────────────────────────
|
||||
var header = CreatePanel(canvasGo.transform, "Header", new Color(0.12f, 0.12f, 0.18f, 0.95f));
|
||||
SetAnchors(header.GetComponent<RectTransform>(), new Vector2(0f, 0.93f), new Vector2(1f, 1f));
|
||||
|
||||
var backBtn = CreateTMPButton(header.transform, "BackButton", "< Back", new Color(0.2f, 0.2f, 0.3f));
|
||||
SetAnchors(backBtn.GetComponent<RectTransform>(), new Vector2(0.01f, 0.1f), new Vector2(0.08f, 0.9f));
|
||||
var title = CreateTMPText(header.transform, "TitleText", "Simulator", 28, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
SetAnchors(title.GetComponent<RectTransform>(), new Vector2(0.3f, 0.1f), new Vector2(0.7f, 0.9f));
|
||||
|
||||
// Config Panel
|
||||
var configPanel = CreatePanel(canvasGo.transform, "ConfigPanel", new Color(0.1f, 0.1f, 0.16f));
|
||||
SetAnchors(configPanel.GetComponent<RectTransform>(), new Vector2(0.05f, 0.35f), new Vector2(0.5f, 0.93f));
|
||||
var titleTxt = CreateTMPText(header.transform, "TitleText", "Simulator", 28, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
SetAnchors(titleTxt.GetComponent<RectTransform>(), new Vector2(0.3f, 0.1f), new Vector2(0.7f, 0.9f));
|
||||
|
||||
var startSimBtn = CreateTMPButton(header.transform, "StartSimButton", "Start Simulation", new Color(0.2f, 0.55f, 0.2f));
|
||||
SetAnchors(startSimBtn.GetComponent<RectTransform>(), new Vector2(0.82f, 0.1f), new Vector2(0.99f, 0.9f));
|
||||
|
||||
// ── Config Panel (left) ───────────────────────────────────────────────
|
||||
var configPanel = CreatePanel(canvasGo.transform, "ConfigPanel", new Color(0.1f, 0.1f, 0.16f, 0.97f));
|
||||
SetAnchors(configPanel.GetComponent<RectTransform>(), new Vector2(0.01f, 0.3f), new Vector2(0.49f, 0.93f));
|
||||
var cfgVlg = configPanel.AddComponent<VerticalLayoutGroup>();
|
||||
cfgVlg.padding = new RectOffset(16, 16, 16, 16); cfgVlg.spacing = 10;
|
||||
cfgVlg.childForceExpandWidth = true; cfgVlg.childForceExpandHeight = false;
|
||||
cfgVlg.padding = new RectOffset(16, 16, 16, 16);
|
||||
cfgVlg.spacing = 10;
|
||||
cfgVlg.childForceExpandWidth = true;
|
||||
cfgVlg.childForceExpandHeight = false;
|
||||
|
||||
CreateTMPText(configPanel.transform, "ConfigTitle", "Simulation Config", 20, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
|
||||
// Deck selector
|
||||
var deckLabel = CreateTMPText(configPanel.transform, "DeckLabel", "Deck:", 16, FontStyles.Normal, TextAlignmentOptions.Left);
|
||||
var deckInput = CreateTMPInputField(configPanel.transform, "DeckInput", "Deck ID or name...");
|
||||
// Deck dropdowns
|
||||
var deckDropdown0 = CreateTMPDropdown(configPanel.transform, "DeckDropdown0", new[] { "-- Select Deck --" });
|
||||
var deckDropdown1 = CreateTMPDropdown(configPanel.transform, "DeckDropdown1", new[] { "-- Select Deck --" });
|
||||
var deckDropdown2 = CreateTMPDropdown(configPanel.transform, "DeckDropdown2", new[] { "-- Select Deck --" });
|
||||
var deckDropdown3 = CreateTMPDropdown(configPanel.transform, "DeckDropdown3", new[] { "-- Select Deck --" });
|
||||
|
||||
// Num games
|
||||
var gamesLabel = CreateTMPText(configPanel.transform, "GamesLabel", "Games:", 16, FontStyles.Normal, TextAlignmentOptions.Left);
|
||||
var gamesInput = CreateTMPInputField(configPanel.transform, "GamesInput", "100");
|
||||
// Num players slider
|
||||
var numPlayersLabel = CreateTMPText(configPanel.transform, "NumPlayersLabel", "Players: 2", 16, FontStyles.Normal, TextAlignmentOptions.Left);
|
||||
var numPlayersSlider = CreateSlider(configPanel.transform, "NumPlayersSlider");
|
||||
|
||||
// Start/Stop buttons
|
||||
var startBtn = CreateTMPButton(configPanel.transform, "StartButton", "Start Simulation", new Color(0.2f, 0.55f, 0.2f));
|
||||
var stopBtn = CreateTMPButton(configPanel.transform, "StopButton", "Stop", new Color(0.55f, 0.2f, 0.2f));
|
||||
// Num games input
|
||||
CreateTMPText(configPanel.transform, "NumGamesLabel", "Number of Games:", 16, FontStyles.Normal, TextAlignmentOptions.Left);
|
||||
var numGamesInput = CreateTMPInputField(configPanel.transform, "NumGamesInput", "100");
|
||||
|
||||
// Results Panel
|
||||
var resultsPanel = CreatePanel(canvasGo.transform, "ResultsPanel", new Color(0.1f, 0.1f, 0.16f));
|
||||
SetAnchors(resultsPanel.GetComponent<RectTransform>(), new Vector2(0.52f, 0.35f), new Vector2(0.95f, 0.93f));
|
||||
// Threads input
|
||||
CreateTMPText(configPanel.transform, "ThreadsLabel", "Threads:", 16, FontStyles.Normal, TextAlignmentOptions.Left);
|
||||
var threadsInput = CreateTMPInputField(configPanel.transform, "ThreadsInput", "4");
|
||||
|
||||
var resultsTitle = CreateTMPText(resultsPanel.transform, "ResultsTitle", "Results", 20, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
SetAnchors(resultsTitle.GetComponent<RectTransform>(), new Vector2(0f, 0.90f), new Vector2(1f, 1f));
|
||||
// AI Profile dropdown
|
||||
CreateTMPText(configPanel.transform, "AiProfileLabel", "AI Profile:", 16, FontStyles.Normal, TextAlignmentOptions.Left);
|
||||
var aiProfileDropdown = CreateTMPDropdown(configPanel.transform, "AiProfileDropdown", new[] { "Default" });
|
||||
|
||||
var statusText = CreateTMPText(resultsPanel.transform, "StatusText", "Ready", 16, FontStyles.Normal, TextAlignmentOptions.TopLeft);
|
||||
SetAnchors(statusText.GetComponent<RectTransform>(), new Vector2(0.03f, 0.03f), new Vector2(0.97f, 0.90f));
|
||||
// ── Status Panel (center, hidden initially) ───────────────────────────
|
||||
var statusPanel = CreatePanel(canvasGo.transform, "StatusPanel", new Color(0.09f, 0.09f, 0.14f, 0.97f));
|
||||
SetAnchors(statusPanel.GetComponent<RectTransform>(), new Vector2(0.01f, 0.06f), new Vector2(0.49f, 0.29f));
|
||||
statusPanel.SetActive(false);
|
||||
var stVlg = statusPanel.AddComponent<VerticalLayoutGroup>();
|
||||
stVlg.padding = new RectOffset(12, 12, 10, 10);
|
||||
stVlg.spacing = 6;
|
||||
stVlg.childForceExpandWidth = true;
|
||||
stVlg.childForceExpandHeight = false;
|
||||
|
||||
// Progress bar
|
||||
var progressBg = CreatePanel(canvasGo.transform, "ProgressBar", new Color(0.2f, 0.2f, 0.25f));
|
||||
SetAnchors(progressBg.GetComponent<RectTransform>(), new Vector2(0.05f, 0.28f), new Vector2(0.95f, 0.33f));
|
||||
var progressFill = CreatePanel(progressBg.transform, "ProgressFill", new Color(0.2f, 0.6f, 0.3f));
|
||||
SetAnchors(progressFill.GetComponent<RectTransform>(), new Vector2(0f, 0f), new Vector2(0f, 1f));
|
||||
var progressText = CreateTMPText(progressBg.transform, "ProgressText", "0%", 14, FontStyles.Normal, TextAlignmentOptions.Center);
|
||||
SetAnchors(progressText.GetComponent<RectTransform>(), new Vector2(0f, 0f), new Vector2(1f, 1f));
|
||||
var statusText = CreateTMPText(statusPanel.transform, "StatusText", "Ready", 16, FontStyles.Normal, TextAlignmentOptions.TopLeft);
|
||||
var progressBar = CreateSlider(statusPanel.transform, "ProgressBar");
|
||||
var progressText = CreateTMPText(statusPanel.transform, "ProgressText", "0%", 14, FontStyles.Normal, TextAlignmentOptions.Center);
|
||||
var elapsedText = CreateTMPText(statusPanel.transform, "ElapsedText", "Elapsed: 0s", 14, FontStyles.Normal, TextAlignmentOptions.Left);
|
||||
var simsPerSecText = CreateTMPText(statusPanel.transform, "SimsPerSecText", "0 sims/s", 14, FontStyles.Normal, TextAlignmentOptions.Left);
|
||||
|
||||
// -- Save --
|
||||
// ── Results Panel (right, hidden initially) ───────────────────────────
|
||||
var resultsPanel = CreatePanel(canvasGo.transform, "ResultsPanel", new Color(0.1f, 0.1f, 0.16f, 0.97f));
|
||||
SetAnchors(resultsPanel.GetComponent<RectTransform>(), new Vector2(0.51f, 0.06f), new Vector2(0.99f, 0.93f));
|
||||
resultsPanel.SetActive(false);
|
||||
|
||||
CreateTMPText(resultsPanel.transform, "ResultsTitle", "Results", 22, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
var winnerText = CreateTMPText(resultsPanel.transform, "WinnerText", "Winner: —", 18, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
SetAnchors(winnerText.GetComponent<RectTransform>(), new Vector2(0f, 0.88f), new Vector2(1f, 0.95f));
|
||||
|
||||
// Results scroll view
|
||||
var resultsScroll = new GameObject("ResultsScroll");
|
||||
resultsScroll.transform.SetParent(resultsPanel.transform, false);
|
||||
SetAnchors(resultsScroll.GetComponent<RectTransform>(), new Vector2(0f, 0.15f), new Vector2(1f, 0.88f));
|
||||
ScrollRect resSr;
|
||||
var resultsContent = CreateScrollContent(resultsScroll, out resSr);
|
||||
|
||||
var historyBtn = CreateTMPButton(resultsPanel.transform, "HistoryButton", "View History", new Color(0.2f, 0.4f, 0.7f));
|
||||
SetAnchors(historyBtn.GetComponent<RectTransform>(), new Vector2(0.2f, 0.03f), new Vector2(0.8f, 0.13f));
|
||||
|
||||
// ── Error Panel (hidden initially) ────────────────────────────────────
|
||||
var errorPanel = CreatePanel(canvasGo.transform, "ErrorPanel", new Color(0.15f, 0.05f, 0.05f, 0.97f));
|
||||
SetAnchors(errorPanel.GetComponent<RectTransform>(), new Vector2(0.25f, 0.35f), new Vector2(0.75f, 0.65f));
|
||||
errorPanel.SetActive(false);
|
||||
|
||||
CreateTMPText(errorPanel.transform, "ErrorTitle", "Error", 22, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
var errorText = CreateTMPText(errorPanel.transform, "ErrorText", "", 16, FontStyles.Normal, TextAlignmentOptions.TopLeft);
|
||||
SetAnchors(errorText.GetComponent<RectTransform>(), new Vector2(0.05f, 0.25f), new Vector2(0.95f, 0.85f));
|
||||
var retryBtn = CreateTMPButton(errorPanel.transform, "RetryButton", "Retry", new Color(0.55f, 0.2f, 0.2f));
|
||||
SetAnchors(retryBtn.GetComponent<RectTransform>(), new Vector2(0.3f, 0.05f), new Vector2(0.7f, 0.22f));
|
||||
|
||||
// ── Result Row Prefab (disabled child object used as prefab) ──────────
|
||||
var resultRowPrefab = new GameObject("ResultRowPrefab");
|
||||
resultRowPrefab.transform.SetParent(canvasGo.transform, false);
|
||||
resultRowPrefab.SetActive(false);
|
||||
var rowImg = resultRowPrefab.AddComponent<Image>();
|
||||
rowImg.color = new Color(0.15f, 0.15f, 0.2f, 0.9f);
|
||||
var rowHlg = resultRowPrefab.AddComponent<HorizontalLayoutGroup>();
|
||||
rowHlg.padding = new RectOffset(8, 8, 4, 4);
|
||||
rowHlg.spacing = 12;
|
||||
rowHlg.childForceExpandWidth = false;
|
||||
rowHlg.childForceExpandHeight = true;
|
||||
var rowLe = resultRowPrefab.AddComponent<LayoutElement>();
|
||||
rowLe.minHeight = 40;
|
||||
|
||||
string[] colNames = { "DeckName", "WinRate", "Wins", "Losses" };
|
||||
float[] colWidths = { 300f, 120f, 100f, 100f };
|
||||
for (int i = 0; i < colNames.Length; i++)
|
||||
{
|
||||
var cell = new GameObject(colNames[i]);
|
||||
cell.transform.SetParent(resultRowPrefab.transform, false);
|
||||
var cellTxt = cell.AddComponent<TextMeshProUGUI>();
|
||||
cellTxt.text = colNames[i];
|
||||
cellTxt.fontSize = 16;
|
||||
cellTxt.color = Color.white;
|
||||
cellTxt.alignment = TextAlignmentOptions.Left;
|
||||
var cellLe = cell.AddComponent<LayoutElement>();
|
||||
cellLe.minWidth = colWidths[i];
|
||||
cellLe.preferredWidth = colWidths[i];
|
||||
cellLe.flexibleWidth = i == 0 ? 1f : 0f;
|
||||
}
|
||||
|
||||
// ── Wire SimulationController via reflection ───────────────────────────
|
||||
var controllerGo = new GameObject("SimulationController");
|
||||
controllerGo.transform.SetParent(canvasGo.transform, false);
|
||||
var controller = controllerGo.AddComponent<CommanderAILab.UI.SimulationController>();
|
||||
|
||||
SetField(controller, "backButton", backBtn.GetComponent<Button>());
|
||||
SetField(controller, "startSimButton", startSimBtn.GetComponent<Button>());
|
||||
|
||||
SetField(controller, "deckDropdown0", deckDropdown0.GetComponent<TMP_Dropdown>());
|
||||
SetField(controller, "deckDropdown1", deckDropdown1.GetComponent<TMP_Dropdown>());
|
||||
SetField(controller, "deckDropdown2", deckDropdown2.GetComponent<TMP_Dropdown>());
|
||||
SetField(controller, "deckDropdown3", deckDropdown3.GetComponent<TMP_Dropdown>());
|
||||
SetField(controller, "numGamesInput", numGamesInput.GetComponent<TMP_InputField>());
|
||||
SetField(controller, "numPlayersSlider", numPlayersSlider.GetComponent<Slider>());
|
||||
SetField(controller, "numPlayersLabel", numPlayersLabel.GetComponent<TMP_Text>());
|
||||
SetField(controller, "threadsInput", threadsInput.GetComponent<TMP_InputField>());
|
||||
SetField(controller, "aiProfileDropdown", aiProfileDropdown.GetComponent<TMP_Dropdown>());
|
||||
|
||||
SetField(controller, "statusText", statusText.GetComponent<TMP_Text>());
|
||||
SetField(controller, "progressBar", progressBar.GetComponent<Slider>());
|
||||
SetField(controller, "progressText", progressText.GetComponent<TMP_Text>());
|
||||
SetField(controller, "elapsedText", elapsedText.GetComponent<TMP_Text>());
|
||||
SetField(controller, "simsPerSecText", simsPerSecText.GetComponent<TMP_Text>());
|
||||
|
||||
SetField(controller, "resultsParent", resultsContent);
|
||||
SetField(controller, "resultRowPrefab", resultRowPrefab);
|
||||
SetField(controller, "winnerText", winnerText.GetComponent<TMP_Text>());
|
||||
SetField(controller, "historyButton", historyBtn.GetComponent<Button>());
|
||||
|
||||
SetField(controller, "errorPanel", errorPanel);
|
||||
SetField(controller, "errorText", errorText.GetComponent<TMP_Text>());
|
||||
SetField(controller, "retryButton", retryBtn.GetComponent<Button>());
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Scenes"))
|
||||
AssetDatabase.CreateFolder("Assets", "Scenes");
|
||||
string path = "Assets/Scenes/Simulator.unity";
|
||||
@@ -93,7 +205,15 @@ public static class CreateSimulatorScene
|
||||
Debug.Log("[CreateSimulatorScene] Simulator scene created and saved to " + path);
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
static void SetField(object target, string fieldName, object value)
|
||||
{
|
||||
var field = target.GetType().GetField(fieldName,
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (field != null) field.SetValue(target, value);
|
||||
else Debug.LogWarning("[CreateSimulatorScene] Could not find field: " + fieldName);
|
||||
}
|
||||
|
||||
static void SetAnchors(RectTransform rt, Vector2 min, Vector2 max)
|
||||
{
|
||||
@@ -119,11 +239,11 @@ public static class CreateSimulatorScene
|
||||
var go = new GameObject(name);
|
||||
go.transform.SetParent(parent, false);
|
||||
var tmp = go.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text;
|
||||
tmp.fontSize = fontSize;
|
||||
tmp.text = text;
|
||||
tmp.fontSize = fontSize;
|
||||
tmp.fontStyle = style;
|
||||
tmp.alignment = alignment;
|
||||
tmp.color = Color.white;
|
||||
tmp.color = Color.white;
|
||||
var rt = go.GetComponent<RectTransform>();
|
||||
SetAnchors(rt, Vector2.zero, Vector2.one);
|
||||
return go;
|
||||
@@ -139,16 +259,16 @@ public static class CreateSimulatorScene
|
||||
btn.targetGraphic = img;
|
||||
var colors = btn.colors;
|
||||
colors.highlightedColor = bgColor * 1.2f;
|
||||
colors.pressedColor = bgColor * 0.8f;
|
||||
colors.pressedColor = bgColor * 0.8f;
|
||||
btn.colors = colors;
|
||||
|
||||
var textGo = new GameObject("Text");
|
||||
textGo.transform.SetParent(go.transform, false);
|
||||
var tmp = textGo.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = label;
|
||||
tmp.fontSize = 22;
|
||||
tmp.text = label;
|
||||
tmp.fontSize = 22;
|
||||
tmp.alignment = TextAlignmentOptions.Center;
|
||||
tmp.color = Color.white;
|
||||
tmp.color = Color.white;
|
||||
SetAnchors(textGo.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
|
||||
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
@@ -172,29 +292,154 @@ public static class CreateSimulatorScene
|
||||
var placeholder = new GameObject("Placeholder");
|
||||
placeholder.transform.SetParent(textArea.transform, false);
|
||||
var phTmp = placeholder.AddComponent<TextMeshProUGUI>();
|
||||
phTmp.text = defaultText;
|
||||
phTmp.fontSize = 18;
|
||||
phTmp.text = defaultText;
|
||||
phTmp.fontSize = 18;
|
||||
phTmp.fontStyle = FontStyles.Italic;
|
||||
phTmp.color = new Color(0.5f, 0.5f, 0.5f, 0.8f);
|
||||
phTmp.color = new Color(0.5f, 0.5f, 0.5f, 0.8f);
|
||||
phTmp.alignment = TextAlignmentOptions.Left;
|
||||
SetAnchors(placeholder.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
|
||||
|
||||
var inputText = new GameObject("Text");
|
||||
inputText.transform.SetParent(textArea.transform, false);
|
||||
var itTmp = inputText.AddComponent<TextMeshProUGUI>();
|
||||
itTmp.fontSize = 18;
|
||||
itTmp.color = Color.white;
|
||||
itTmp.fontSize = 18;
|
||||
itTmp.color = Color.white;
|
||||
itTmp.alignment = TextAlignmentOptions.Left;
|
||||
SetAnchors(inputText.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
|
||||
|
||||
var inputField = go.AddComponent<TMP_InputField>();
|
||||
inputField.textViewport = textArea.GetComponent<RectTransform>();
|
||||
inputField.textComponent = itTmp;
|
||||
inputField.placeholder = phTmp;
|
||||
inputField.text = defaultText;
|
||||
inputField.fontAsset = itTmp.font;
|
||||
inputField.placeholder = phTmp;
|
||||
inputField.text = defaultText;
|
||||
inputField.fontAsset = itTmp.font;
|
||||
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
le.minHeight = 45;
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
static GameObject CreateTMPDropdown(Transform parent, string name, string[] options)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
go.transform.SetParent(parent, false);
|
||||
var img = go.AddComponent<Image>();
|
||||
img.color = new Color(0.15f, 0.15f, 0.2f);
|
||||
|
||||
var labelGo = new GameObject("Label");
|
||||
labelGo.transform.SetParent(go.transform, false);
|
||||
var labelTmp = labelGo.AddComponent<TextMeshProUGUI>();
|
||||
labelTmp.text = options.Length > 0 ? options[0] : "";
|
||||
labelTmp.fontSize = 16;
|
||||
labelTmp.alignment = TextAlignmentOptions.Left;
|
||||
labelTmp.color = Color.white;
|
||||
SetAnchors(labelGo.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
|
||||
|
||||
var dd = go.AddComponent<TMP_Dropdown>();
|
||||
dd.captionText = labelTmp;
|
||||
dd.ClearOptions();
|
||||
dd.AddOptions(new System.Collections.Generic.List<string>(options));
|
||||
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
le.minHeight = 40;
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
static GameObject CreateSlider(Transform parent, string name)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
go.transform.SetParent(parent, false);
|
||||
|
||||
// Background track
|
||||
var bg = new GameObject("Background");
|
||||
bg.transform.SetParent(go.transform, false);
|
||||
var bgImg = bg.AddComponent<Image>();
|
||||
bgImg.color = new Color(0.2f, 0.2f, 0.25f);
|
||||
SetAnchors(bg.GetComponent<RectTransform>(), new Vector2(0f, 0.25f), new Vector2(1f, 0.75f));
|
||||
|
||||
// Fill area
|
||||
var fillArea = new GameObject("Fill Area");
|
||||
fillArea.transform.SetParent(go.transform, false);
|
||||
SetAnchors(fillArea.GetComponent<RectTransform>(), new Vector2(0f, 0.25f), new Vector2(1f, 0.75f));
|
||||
|
||||
var fill = new GameObject("Fill");
|
||||
fill.transform.SetParent(fillArea.transform, false);
|
||||
var fillImg = fill.AddComponent<Image>();
|
||||
fillImg.color = new Color(0.2f, 0.6f, 0.3f);
|
||||
var fillRt = fill.GetComponent<RectTransform>();
|
||||
fillRt.anchorMin = Vector2.zero;
|
||||
fillRt.anchorMax = Vector2.one;
|
||||
fillRt.offsetMin = Vector2.zero;
|
||||
fillRt.offsetMax = Vector2.zero;
|
||||
|
||||
// Handle
|
||||
var handleArea = new GameObject("Handle Slide Area");
|
||||
handleArea.transform.SetParent(go.transform, false);
|
||||
SetAnchors(handleArea.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
|
||||
|
||||
var handle = new GameObject("Handle");
|
||||
handle.transform.SetParent(handleArea.transform, false);
|
||||
var handleImg = handle.AddComponent<Image>();
|
||||
handleImg.color = new Color(0.8f, 0.8f, 0.9f);
|
||||
var handleRt = handle.GetComponent<RectTransform>();
|
||||
handleRt.sizeDelta = new Vector2(20f, 20f);
|
||||
|
||||
var slider = go.AddComponent<Slider>();
|
||||
slider.fillRect = fillRt;
|
||||
slider.handleRect = handleRt;
|
||||
slider.targetGraphic = handleImg;
|
||||
slider.direction = Slider.Direction.LeftToRight;
|
||||
slider.minValue = 0f;
|
||||
slider.maxValue = 1f;
|
||||
slider.value = 0f;
|
||||
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
le.minHeight = 30;
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Viewport + Content hierarchy inside the given scroll GameObject,
|
||||
/// wires up the ScrollRect, and returns the Content RectTransform.
|
||||
/// </summary>
|
||||
static RectTransform CreateScrollContent(GameObject scrollGo, out ScrollRect scrollRect)
|
||||
{
|
||||
scrollRect = scrollGo.GetComponent<ScrollRect>();
|
||||
if (scrollRect == null)
|
||||
scrollRect = scrollGo.AddComponent<ScrollRect>();
|
||||
|
||||
var viewport = new GameObject("Viewport");
|
||||
viewport.transform.SetParent(scrollGo.transform, false);
|
||||
viewport.AddComponent<RectMask2D>();
|
||||
var vpRect = viewport.GetComponent<RectTransform>();
|
||||
vpRect.anchorMin = Vector2.zero;
|
||||
vpRect.anchorMax = Vector2.one;
|
||||
vpRect.offsetMin = Vector2.zero;
|
||||
vpRect.offsetMax = Vector2.zero;
|
||||
|
||||
var content = new GameObject("Content");
|
||||
content.transform.SetParent(viewport.transform, false);
|
||||
var contentRect = content.AddComponent<RectTransform>();
|
||||
contentRect.anchorMin = new Vector2(0f, 1f);
|
||||
contentRect.anchorMax = new Vector2(1f, 1f);
|
||||
contentRect.pivot = new Vector2(0.5f, 1f);
|
||||
contentRect.offsetMin = Vector2.zero;
|
||||
contentRect.offsetMax = Vector2.zero;
|
||||
content.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
var vlg = content.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.spacing = 4;
|
||||
vlg.padding = new RectOffset(4, 4, 4, 4);
|
||||
vlg.childForceExpandWidth = true;
|
||||
vlg.childForceExpandHeight = false;
|
||||
|
||||
scrollRect.viewport = vpRect;
|
||||
scrollRect.content = contentRect;
|
||||
scrollRect.horizontal = false;
|
||||
|
||||
return contentRect;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -204,7 +204,7 @@ namespace CommanderAILab.Services
|
||||
|
||||
public Coroutine GetDecks(Action<string> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
return StartCoroutine(GetRawCoroutine("/api/decks", onSuccess, onError));
|
||||
return StartCoroutine(GetRawCoroutine("/api/lab/decks", onSuccess, onError));
|
||||
}
|
||||
|
||||
public Coroutine CreateDeck(string deckJson, Action<string> onSuccess, Action<string> onError = null)
|
||||
@@ -214,6 +214,11 @@ namespace CommanderAILab.Services
|
||||
|
||||
// -- Simulator ------------------------------------------------------
|
||||
|
||||
public Coroutine GetProfiles(Action<string> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
return StartCoroutine(GetRawCoroutine("/api/lab/profiles", onSuccess, onError));
|
||||
}
|
||||
|
||||
public Coroutine StartSim(string configJson, Action<string> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
return PostRaw("/api/lab/start", configJson, onSuccess, onError);
|
||||
@@ -221,12 +226,12 @@ namespace CommanderAILab.Services
|
||||
|
||||
public Coroutine GetSimStatus(string simId, Action<string> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
return StartCoroutine(GetRawCoroutine($"/api/lab/status/{simId}", onSuccess, onError));
|
||||
return StartCoroutine(GetRawCoroutine($"/api/lab/status?batchId={simId}", onSuccess, onError));
|
||||
}
|
||||
|
||||
public Coroutine GetSimResults(string simId, Action<string> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
return StartCoroutine(GetRawCoroutine($"/api/lab/results/{simId}", onSuccess, onError));
|
||||
return StartCoroutine(GetRawCoroutine($"/api/lab/result?batchId={simId}", onSuccess, onError));
|
||||
}
|
||||
|
||||
// -- Coach ----------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
using TMPro;
|
||||
using Newtonsoft.Json;
|
||||
using CommanderAILab.Models;
|
||||
using CommanderAILab.Services;
|
||||
|
||||
namespace CommanderAILab.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulator scene — configure and run multi-player simulations,
|
||||
/// poll status, display results per deck.
|
||||
/// </summary>
|
||||
public class SimulationController : MonoBehaviour
|
||||
{
|
||||
// ── Navigation ───────────────────────────────────────────────────────
|
||||
[Header("Navigation")]
|
||||
[SerializeField] private Button backButton;
|
||||
[SerializeField] private Button startSimButton;
|
||||
|
||||
// ── Config Panel ──────────────────────────────────────────────────────
|
||||
[Header("Config")]
|
||||
[SerializeField] private TMP_Dropdown deckDropdown0;
|
||||
[SerializeField] private TMP_Dropdown deckDropdown1;
|
||||
[SerializeField] private TMP_Dropdown deckDropdown2;
|
||||
[SerializeField] private TMP_Dropdown deckDropdown3;
|
||||
[SerializeField] private TMP_InputField numGamesInput;
|
||||
[SerializeField] private Slider numPlayersSlider;
|
||||
[SerializeField] private TMP_Text numPlayersLabel;
|
||||
[SerializeField] private TMP_InputField threadsInput;
|
||||
[SerializeField] private TMP_Dropdown aiProfileDropdown;
|
||||
|
||||
// ── Status Panel ──────────────────────────────────────────────────────
|
||||
[Header("Status")]
|
||||
[SerializeField] private TMP_Text statusText;
|
||||
[SerializeField] private Slider progressBar;
|
||||
[SerializeField] private TMP_Text progressText;
|
||||
[SerializeField] private TMP_Text elapsedText;
|
||||
[SerializeField] private TMP_Text simsPerSecText;
|
||||
|
||||
// ── Results Panel ─────────────────────────────────────────────────────
|
||||
[Header("Results")]
|
||||
[SerializeField] private Transform resultsParent;
|
||||
[SerializeField] private GameObject resultRowPrefab;
|
||||
[SerializeField] private TMP_Text winnerText;
|
||||
[SerializeField] private Button historyButton;
|
||||
|
||||
// ── Error ─────────────────────────────────────────────────────────────
|
||||
[Header("Error")]
|
||||
[SerializeField] private GameObject errorPanel;
|
||||
[SerializeField] private TMP_Text errorText;
|
||||
[SerializeField] private Button retryButton;
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────
|
||||
private ApiClient _api;
|
||||
private List<LabDeckEntry> _decks = new();
|
||||
private List<AiProfile> _profiles = new();
|
||||
private string _currentBatchId;
|
||||
private bool _isPolling;
|
||||
|
||||
private readonly List<GameObject> _resultRowObjects = new();
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
private void Start()
|
||||
{
|
||||
_api = FindObjectOfType<ApiClient>();
|
||||
|
||||
backButton.onClick.AddListener(() => SceneManager.LoadScene("MainMenu"));
|
||||
startSimButton.onClick.AddListener(OnStartSim);
|
||||
retryButton.onClick.AddListener(OnStartSim);
|
||||
|
||||
numPlayersSlider.minValue = 2;
|
||||
numPlayersSlider.maxValue = 4;
|
||||
numPlayersSlider.wholeNumbers = true;
|
||||
numPlayersSlider.value = 2;
|
||||
numPlayersSlider.onValueChanged.AddListener(OnNumPlayersChanged);
|
||||
OnNumPlayersChanged(numPlayersSlider.value);
|
||||
|
||||
errorPanel.SetActive(false);
|
||||
|
||||
// Check for a deck exported from DeckBuilder
|
||||
string simDeck = PlayerPrefs.GetString("SimDeck", "");
|
||||
if (!string.IsNullOrEmpty(simDeck))
|
||||
{
|
||||
PlayerPrefs.DeleteKey("SimDeck");
|
||||
// Pre-populate deck dropdown 0 after decks load — stored for later
|
||||
PlayerPrefs.SetString("_SimDeckPending", simDeck);
|
||||
}
|
||||
|
||||
LoadDecks();
|
||||
LoadAiProfiles();
|
||||
}
|
||||
|
||||
// ── Player Count ──────────────────────────────────────────────────────
|
||||
private void OnNumPlayersChanged(float value)
|
||||
{
|
||||
int count = Mathf.RoundToInt(value);
|
||||
if (numPlayersLabel != null)
|
||||
numPlayersLabel.text = $"Players: {count}";
|
||||
|
||||
// Show/hide optional deck dropdowns based on count
|
||||
if (deckDropdown2 != null) deckDropdown2.gameObject.SetActive(count >= 3);
|
||||
if (deckDropdown3 != null) deckDropdown3.gameObject.SetActive(count >= 4);
|
||||
}
|
||||
|
||||
// ── Deck List ─────────────────────────────────────────────────────────
|
||||
private void LoadDecks()
|
||||
{
|
||||
_api.GetDecks(
|
||||
json =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var wrapper = JsonConvert.DeserializeObject<DecksWrapper>(json);
|
||||
_decks = wrapper?.decks ?? new List<LabDeckEntry>();
|
||||
}
|
||||
catch { _decks = new List<LabDeckEntry>(); }
|
||||
PopulateDeckDropdowns();
|
||||
},
|
||||
err => ShowError($"Failed to load decks:\n{err}"));
|
||||
}
|
||||
|
||||
private void PopulateDeckDropdowns()
|
||||
{
|
||||
var options = new List<string> { "-- Select Deck --" };
|
||||
foreach (var d in _decks) options.Add(d.name);
|
||||
|
||||
void Fill(TMP_Dropdown dd)
|
||||
{
|
||||
if (dd == null) return;
|
||||
dd.ClearOptions();
|
||||
dd.AddOptions(options);
|
||||
dd.value = 0;
|
||||
}
|
||||
|
||||
Fill(deckDropdown0);
|
||||
Fill(deckDropdown1);
|
||||
Fill(deckDropdown2);
|
||||
Fill(deckDropdown3);
|
||||
|
||||
// Apply any pending deck from DeckBuilder export
|
||||
string pending = PlayerPrefs.GetString("_SimDeckPending", "");
|
||||
if (!string.IsNullOrEmpty(pending))
|
||||
{
|
||||
PlayerPrefs.DeleteKey("_SimDeckPending");
|
||||
try
|
||||
{
|
||||
// Try to match by name from the JSON
|
||||
var exported = JsonConvert.DeserializeObject<LabDeckEntry>(pending);
|
||||
if (exported != null && !string.IsNullOrEmpty(exported.name))
|
||||
{
|
||||
int idx = options.IndexOf(exported.name);
|
||||
if (idx >= 0 && deckDropdown0 != null)
|
||||
deckDropdown0.value = idx;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// ── AI Profiles ───────────────────────────────────────────────────────
|
||||
private void LoadAiProfiles()
|
||||
{
|
||||
_api.GetProfiles(
|
||||
json =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var wrapper = JsonConvert.DeserializeObject<ProfilesWrapper>(json);
|
||||
_profiles = wrapper?.profiles ?? new List<AiProfile>();
|
||||
}
|
||||
catch { _profiles = new List<AiProfile>(); }
|
||||
PopulateProfileDropdown();
|
||||
},
|
||||
err =>
|
||||
{
|
||||
// Non-fatal: profiles may not be available yet
|
||||
_profiles = new List<AiProfile>();
|
||||
PopulateProfileDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
private void PopulateProfileDropdown()
|
||||
{
|
||||
if (aiProfileDropdown == null) return;
|
||||
aiProfileDropdown.ClearOptions();
|
||||
var options = new List<string>();
|
||||
foreach (var p in _profiles) options.Add(p.name);
|
||||
if (options.Count == 0) options.Add("Default");
|
||||
aiProfileDropdown.AddOptions(options);
|
||||
aiProfileDropdown.value = 0;
|
||||
}
|
||||
|
||||
// ── Start Simulation ──────────────────────────────────────────────────
|
||||
private void OnStartSim()
|
||||
{
|
||||
errorPanel.SetActive(false);
|
||||
|
||||
int numPlayers = Mathf.RoundToInt(numPlayersSlider.value);
|
||||
|
||||
// Collect selected deck names (index 0 = "-- Select Deck --")
|
||||
var selectedDecks = new List<string>();
|
||||
void TryAdd(TMP_Dropdown dd)
|
||||
{
|
||||
if (dd == null || !dd.gameObject.activeSelf) return;
|
||||
if (dd.value <= 0) return;
|
||||
int deckIdx = dd.value - 1;
|
||||
if (deckIdx < _decks.Count)
|
||||
selectedDecks.Add(_decks[deckIdx].name);
|
||||
}
|
||||
|
||||
TryAdd(deckDropdown0);
|
||||
TryAdd(deckDropdown1);
|
||||
TryAdd(deckDropdown2);
|
||||
TryAdd(deckDropdown3);
|
||||
|
||||
if (selectedDecks.Count < 2)
|
||||
{
|
||||
ShowError("Please select at least 2 decks to simulate.");
|
||||
return;
|
||||
}
|
||||
if (selectedDecks.Count < numPlayers)
|
||||
{
|
||||
ShowError($"Please select {numPlayers} decks for {numPlayers}-player simulation.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(numGamesInput.text, out int numGames) || numGames <= 0)
|
||||
numGames = 100;
|
||||
|
||||
if (!int.TryParse(threadsInput.text, out int threads) || threads <= 0)
|
||||
threads = 4;
|
||||
|
||||
string policyStyle = aiProfileDropdown != null && _profiles.Count > 0
|
||||
? _profiles[Mathf.Clamp(aiProfileDropdown.value, 0, _profiles.Count - 1)].name
|
||||
: "Default";
|
||||
|
||||
var request = new StartRequest
|
||||
{
|
||||
decks = selectedDecks,
|
||||
numGames = numGames,
|
||||
threads = threads,
|
||||
useLearnedPolicy = false,
|
||||
policyStyle = policyStyle,
|
||||
seed = 0,
|
||||
clock = 0
|
||||
};
|
||||
|
||||
string json = JsonConvert.SerializeObject(request);
|
||||
|
||||
SetStatusPanelVisible(true);
|
||||
SetResultsPanelVisible(false);
|
||||
if (statusText) statusText.text = "Starting simulation...";
|
||||
if (progressBar) progressBar.value = 0f;
|
||||
if (progressText) progressText.text = "0%";
|
||||
startSimButton.interactable = false;
|
||||
|
||||
_api.StartSim(json,
|
||||
respJson =>
|
||||
{
|
||||
SimStartResponse resp;
|
||||
try { resp = JsonConvert.DeserializeObject<SimStartResponse>(respJson); }
|
||||
catch { ShowError("Failed to parse start response."); startSimButton.interactable = true; return; }
|
||||
|
||||
if (resp == null || string.IsNullOrEmpty(resp.batchId))
|
||||
{
|
||||
ShowError("Invalid response from server.");
|
||||
startSimButton.interactable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_currentBatchId = resp.batchId;
|
||||
if (!_isPolling)
|
||||
StartCoroutine(PollStatus(_currentBatchId));
|
||||
},
|
||||
err => { ShowError($"Failed to start simulation:\n{err}"); startSimButton.interactable = true; });
|
||||
}
|
||||
|
||||
// ── Poll Status ───────────────────────────────────────────────────────
|
||||
private IEnumerator PollStatus(string batchId)
|
||||
{
|
||||
_isPolling = true;
|
||||
|
||||
while (true)
|
||||
{
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
bool done = false;
|
||||
string errorMsg = null;
|
||||
SimStatusModel latestStatus = null;
|
||||
|
||||
_api.GetSimStatus(batchId,
|
||||
json =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = JsonConvert.DeserializeObject<SimStatusModel>(json);
|
||||
if (status == null) { done = true; errorMsg = "Empty status response."; return; }
|
||||
latestStatus = status;
|
||||
|
||||
if (!string.IsNullOrEmpty(status.error))
|
||||
{
|
||||
done = true;
|
||||
errorMsg = status.error;
|
||||
return;
|
||||
}
|
||||
|
||||
int total = status.total > 0 ? status.total
|
||||
: status.totalGames > 0 ? status.totalGames : 1;
|
||||
int completed = status.completed > 0 ? status.completed
|
||||
: status.gamesCompleted;
|
||||
|
||||
float progress = total > 0 ? (float)completed / total : 0f;
|
||||
if (progressBar) progressBar.value = progress;
|
||||
if (progressText) progressText.text = $"{Mathf.RoundToInt(progress * 100)}%";
|
||||
if (statusText) statusText.text = $"Running... {completed}/{total} games";
|
||||
if (elapsedText) elapsedText.text = $"Elapsed: {status.elapsedMs / 1000f:F1}s";
|
||||
if (simsPerSecText) simsPerSecText.text = $"{status.simsPerSec:F1} sims/s";
|
||||
|
||||
if (!status.running) done = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
done = true;
|
||||
errorMsg = $"Status parse error: {ex.Message}";
|
||||
}
|
||||
},
|
||||
err => { done = true; errorMsg = err; });
|
||||
|
||||
// Wait one frame so callbacks have fired
|
||||
yield return null;
|
||||
|
||||
if (!string.IsNullOrEmpty(errorMsg))
|
||||
{
|
||||
ShowError($"Simulation error:\n{errorMsg}");
|
||||
startSimButton.interactable = true;
|
||||
_isPolling = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (done)
|
||||
{
|
||||
if (statusText) statusText.text = "Simulation complete!";
|
||||
if (progressBar) progressBar.value = 1f;
|
||||
if (progressText) progressText.text = "100%";
|
||||
startSimButton.interactable = true;
|
||||
_isPolling = false;
|
||||
LoadResults(batchId);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Load Results ──────────────────────────────────────────────────────
|
||||
private void LoadResults(string batchId)
|
||||
{
|
||||
_api.GetSimResults(batchId,
|
||||
json =>
|
||||
{
|
||||
SimResult result;
|
||||
try { result = JsonConvert.DeserializeObject<SimResult>(json); }
|
||||
catch { ShowError("Failed to parse results."); return; }
|
||||
|
||||
if (result?.summary?.perDeck == null) { ShowError("No result data returned."); return; }
|
||||
|
||||
SetResultsPanelVisible(true);
|
||||
BuildResultRows(result.summary.perDeck);
|
||||
},
|
||||
err => ShowError($"Failed to load results:\n{err}"));
|
||||
}
|
||||
|
||||
private void BuildResultRows(List<ResultEntry> entries)
|
||||
{
|
||||
foreach (var go in _resultRowObjects) if (go) Destroy(go);
|
||||
_resultRowObjects.Clear();
|
||||
|
||||
ResultEntry winner = null;
|
||||
foreach (var e in entries)
|
||||
if (winner == null || e.winRate > winner.winRate) winner = e;
|
||||
|
||||
if (winnerText && winner != null)
|
||||
winnerText.text = $"Winner: {winner.deckName} ({winner.winRate * 100f:F1}% win rate)";
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var row = Instantiate(resultRowPrefab, resultsParent);
|
||||
row.SetActive(true);
|
||||
_resultRowObjects.Add(row);
|
||||
|
||||
var labels = row.GetComponentsInChildren<TMP_Text>();
|
||||
if (labels.Length > 0) labels[0].text = entry.deckName;
|
||||
if (labels.Length > 1) labels[1].text = $"{entry.winRate * 100f:F1}%";
|
||||
if (labels.Length > 2) labels[2].text = $"W: {entry.wins}";
|
||||
if (labels.Length > 3) labels[3].text = $"L: {entry.losses}";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Panel Visibility ──────────────────────────────────────────────────
|
||||
private void SetStatusPanelVisible(bool visible)
|
||||
{
|
||||
if (statusText) statusText.transform.parent?.gameObject.SetActive(visible);
|
||||
if (progressBar) progressBar.gameObject.SetActive(visible);
|
||||
if (progressText) progressText.gameObject.SetActive(visible);
|
||||
if (elapsedText) elapsedText.gameObject.SetActive(visible);
|
||||
if (simsPerSecText) simsPerSecText.gameObject.SetActive(visible);
|
||||
}
|
||||
|
||||
private void SetResultsPanelVisible(bool visible)
|
||||
{
|
||||
if (resultsParent) resultsParent.gameObject.SetActive(visible);
|
||||
if (winnerText) winnerText.gameObject.SetActive(visible);
|
||||
if (historyButton) historyButton.gameObject.SetActive(visible);
|
||||
}
|
||||
|
||||
// ── Error ─────────────────────────────────────────────────────────────
|
||||
private void ShowError(string msg)
|
||||
{
|
||||
errorPanel.SetActive(true);
|
||||
errorText.text = msg;
|
||||
}
|
||||
|
||||
// ── Helper Models (private inner classes) ─────────────────────────────
|
||||
[Serializable] private class DecksWrapper { public List<LabDeckEntry> decks; }
|
||||
[Serializable] private class LabDeckEntry { public string name; public string source; public string commander; }
|
||||
[Serializable] private class ProfilesWrapper { public List<AiProfile> profiles; }
|
||||
[Serializable] private class AiProfile { public string name; public string description; }
|
||||
[Serializable] private class StartRequest
|
||||
{
|
||||
public List<string> decks; public int numGames; public int threads;
|
||||
public bool useLearnedPolicy; public string policyStyle;
|
||||
public int seed; public int clock;
|
||||
}
|
||||
[Serializable] private class SimResult { public Summary summary; }
|
||||
[Serializable] private class Summary { public List<ResultEntry> perDeck; }
|
||||
[Serializable] private class ResultEntry { public string deckName; public float winRate; public int wins; public int losses; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c22d6ab09a0439a9e596b46c9d8adaa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user