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 { /// /// Simulator scene — configure and run multi-player simulations, /// poll status, display results per deck. /// Also provides a Play Live button that routes to the Battleground scene. /// public class SimulationController : MonoBehaviour { // ── Navigation ─────────────────────────────────────────────────────── [Header("Navigation")] [SerializeField] private Button backButton; [SerializeField] private Button startSimButton; /// Routes to the Battleground scene for a live 4-player game. [SerializeField] private Button playLiveButton; // ── 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 _decks = new(); private List _profiles = new(); private string _currentBatchId; private bool _isPolling; private readonly List _resultRowObjects = new(); // ── Lifecycle ───────────────────────────────────────────────────────── private void Start() { _api = FindObjectOfType(); backButton.onClick.AddListener(() => SceneManager.LoadScene("MainMenu")); startSimButton.onClick.AddListener(OnStartSim); retryButton.onClick.AddListener(OnStartSim); // Play Live: save current deck selections and load Battleground scene playLiveButton?.onClick.AddListener(OnPlayLive); numPlayersSlider.minValue = 2; numPlayersSlider.maxValue = 4; numPlayersSlider.wholeNumbers = true; numPlayersSlider.value = 2; numPlayersSlider.onValueChanged.AddListener(OnNumPlayersChanged); OnNumPlayersChanged(numPlayersSlider.value); errorPanel.SetActive(false); string simDeck = PlayerPrefs.GetString("SimDeck", ""); if (!string.IsNullOrEmpty(simDeck)) { PlayerPrefs.DeleteKey("SimDeck"); PlayerPrefs.SetString("_SimDeckPending", simDeck); } LoadDecks(); LoadAiProfiles(); } // ── Play Live ───────────────────────────────────────────────────────── private void OnPlayLive() { // Persist current deck selections so LobbySetupModal can pre-populate var seats = new List(); void AddSeat(TMP_Dropdown dd, bool isHuman, int seatIdx) { string deckName = ""; if (dd != null && dd.value > 0 && (dd.value - 1) < _decks.Count) deckName = _decks[dd.value - 1].name; seats.Add(new { isHuman, deckName, aiStyle = "Aggro" }); } AddSeat(deckDropdown0, true, 0); AddSeat(deckDropdown1, false, 1); AddSeat(deckDropdown2, false, 2); AddSeat(deckDropdown3, false, 3); PlayerPrefs.SetString("BattleSeats", JsonConvert.SerializeObject(seats)); PlayerPrefs.Save(); SceneManager.LoadScene("Battleground"); } // ── Player Count ────────────────────────────────────────────────────── private void OnNumPlayersChanged(float value) { int count = Mathf.RoundToInt(value); if (numPlayersLabel != null) numPlayersLabel.text = $"Players: {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(json); _decks = wrapper?.decks ?? new List(); } catch { _decks = new List(); } PopulateDeckDropdowns(); }, err => ShowError($"Failed to load decks:\n{err}")); } private void PopulateDeckDropdowns() { var options = new List { "-- 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); string pending = PlayerPrefs.GetString("_SimDeckPending", ""); if (!string.IsNullOrEmpty(pending)) { PlayerPrefs.DeleteKey("_SimDeckPending"); try { var exported = JsonConvert.DeserializeObject(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(json); _profiles = wrapper?.profiles ?? new List(); } catch { _profiles = new List(); } PopulateProfileDropdown(); }, err => { _profiles = new List(); PopulateProfileDropdown(); }); } private void PopulateProfileDropdown() { if (aiProfileDropdown == null) return; aiProfileDropdown.ClearOptions(); var options = new List(); 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); var selectedDecks = new List(); 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(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; _api.GetSimStatus(batchId, json => { try { var status = JsonConvert.DeserializeObject(json); if (status == null) { done = true; errorMsg = "Empty status response."; return; } 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; }); 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(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 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(); 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 ───────────────────────────────────────────────────── [Serializable] private class DecksWrapper { public List decks; } [Serializable] private class LabDeckEntry { public string name; public string source; public string commander; } [Serializable] private class ProfilesWrapper { public List profiles; } [Serializable] private class AiProfile { public string name; public string description; } [Serializable] private class StartRequest { public List decks; public int numGames; public int threads; public bool useLearnedPolicy; public string policyStyle; public int seed; public int clock; } [Serializable] private class SimStartResponse { public string batchId; } [Serializable] private class SimStatusModel { public bool running; public int total; public int completed; public int totalGames; public int gamesCompleted; public float elapsedMs; public float simsPerSec; public string error; } [Serializable] private class SimResult { public Summary summary; } [Serializable] private class Summary { public List perDeck; } [Serializable] private class ResultEntry { public string deckName; public float winRate; public int wins; public int losses; } } }