using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public enum UGameState { Uninitialized, Playing, Win, Lose } public class UntangleGameManager : MonoBehaviour { [Header("Prefabs & Parents")] public GameObject pointPrefab; public LineRenderer linePrefab; public Transform spawnParent; [Header("UI Elements")] public Text statusText; // public GameObject winUI; // public GameObject loseUI; public Button newGameButton; public Button solveButton; [Header("Audio")] public AudioClip winSound; // public AudioClip loseSound; [Header("Game Settings")] public int pointCount = 10; // Runtime state public UGameState gameState = UGameState.Uninitialized; private List points = new List(); public List connections = new List(); private Vector3[] solutionPositions; private void Awake() { newGameButton.onClick.AddListener(StartGame); solveButton.onClick.AddListener(AutoSolve); } private void Start() { StartGame(); } public void SetGameState(UGameState state) { gameState = state; } public void StartGame() { ResetPuzzle(); } public void ResetPuzzle() { gameState = UGameState.Uninitialized; statusText.text = "Generating new game..."; GenerateRandomGame(); gameState = UGameState.Playing; statusText.text = ""; } void GenerateRandomGame() { ClearOldGame(); statusText.text = ""; points.Clear(); connections.Clear(); solutionPositions = new Vector3[pointCount]; // Step 1: Place points randomly (solution layout) for (int i = 0; i < pointCount; i++) { Vector2 pos; int tries = 0; do { pos = Random.insideUnitCircle * 4f; tries++; if (tries > 1000) break; } while (IsTooClose(pos)); // instantiate as child of spawnParent Vector3 worldPos = new Vector3(pos.x, pos.y, 0f); GameObject obj = Instantiate(pointPrefab, worldPos, Quaternion.identity, spawnParent); Point p = obj.GetComponent(); p.id = i; p.manager = this; points.Add(p); solutionPositions[i] = worldPos; } // Step 2: Create a Minimum Spanning Tree (MST) List<(int, int, float)> edges = new List<(int, int, float)>(); for (int i = 0; i < pointCount; i++) { for (int j = i + 1; j < pointCount; j++) { float dist = Vector2.Distance(points[i].transform.position, points[j].transform.position); edges.Add((i, j, dist)); } } edges.Sort((a, b) => a.Item3.CompareTo(b.Item3)); int[] parent = new int[pointCount]; for (int i = 0; i < pointCount; i++) parent[i] = i; int Find(int x) => parent[x] == x ? x : parent[x] = Find(parent[x]); void Union(int x, int y) => parent[Find(x)] = Find(y); int edgeCount = 0; foreach (var (a, b, _) in edges) { if (Find(a) != Find(b)) { Union(a, b); AddConnection(a, b); edgeCount++; if (edgeCount == pointCount - 1) break; } } // Step 3: Ensure every point has at least degree 2 int[] degrees = new int[pointCount]; foreach (var c in connections) { degrees[c.a.id]++; degrees[c.b.id]++; } for (int i = 0; i < pointCount; i++) { while (degrees[i] < 2) { bool added = false; // Try to add a non-crossing edge for (int j = 0; j < pointCount; j++) { if (i == j || IsConnected(i, j)) continue; Connection temp = new Connection { a = points[i], b = points[j] }; if (DoesIntersectAny(temp)) continue; AddConnection(i, j); degrees[i]++; degrees[j]++; added = true; break; } // If no valid non-crossing edge found, allow one forced connection (rare case) if (!added) { for (int j = 0; j < pointCount; j++) { if (i == j || IsConnected(i, j)) continue; AddConnection(i, j); degrees[i]++; degrees[j]++; break; } } } } // Step 4: Shuffle point positions to create the puzzle foreach (var p in points) p.transform.localPosition = Random.insideUnitCircle * 400f; // localPosition keeps them under spawnParent foreach (var c in connections) c.UpdateLine(); } private void AddConnection(int a, int b) { var c = new Connection { a = points[a], b = points[b] }; var lr = Instantiate(linePrefab, spawnParent); c.line = lr; c.UpdateLine(); connections.Add(c); } private bool IsConnected(int a, int b) { return connections.Exists(c => (c.a.id == a && c.b.id == b) || (c.a.id == b && c.b.id == a)); } private bool DoesIntersectAny(Connection cand) { foreach (var c in connections) { if (cand.SharesPointWith(c)) continue; if (LinesIntersect(cand, c)) return true; } return false; } private bool IsTooClose(Vector2 pos) { return points.Exists(p => Vector2.Distance(p.transform.position, pos) < 0.5f); } private void ClearOldGame() { foreach (var p in points) Destroy(p.gameObject); foreach (var c in connections) Destroy(c.line.gameObject); } public void CheckIfSolved() { if (gameState != UGameState.Playing) return; foreach (var c1 in connections) foreach (var c2 in connections) if (c1 != c2 && !c1.SharesPointWith(c2) && LinesIntersect(c1, c2)) return; // no intersections found! statusText.text = "Completed!"; // if (loseUI != null) // loseUI.SetActive(false); // if (winUI != null) // winUI.SetActive(true); SetGameState(UGameState.Win); Debug.Log("Puzzle Untangle solved!"); if (winSound != null) { SoundFXManager.instance.PlaySound(winSound, transform, 1f); } } private void AutoSolve() { if (gameState != UGameState.Playing) return; for (int i = 0; i < points.Count; i++) points[i].transform.position = solutionPositions[i]; connections.ForEach(c => c.UpdateLine()); CheckIfSolved(); } #region Line‑intersection helpers private bool LinesIntersect(Connection c1, Connection c2) { Vector2 p1 = c1.a.transform.position; Vector2 q1 = c1.b.transform.position; Vector2 p2 = c2.a.transform.position; Vector2 q2 = c2.b.transform.position; return DoIntersect(p1, q1, p2, q2); } private bool DoIntersect(Vector2 a, Vector2 b, Vector2 c, Vector2 d) { int o1 = Orientation(a, b, c); int o2 = Orientation(a, b, d); int o3 = Orientation(c, d, a); int o4 = Orientation(c, d, b); return o1 != o2 && o3 != o4; } private int Orientation(Vector2 a, Vector2 b, Vector2 c) { float v = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y); if (Mathf.Abs(v) < 1e-4f) return 0; return (v > 0) ? 1 : 2; } #endregion }