StellarXipher/Assets/Puzzles/Untangle/Scripts/UntangleGameManager.cs
EthanPisani a52cc24d0d
Some checks failed
Build project / Build for (StandaloneLinux64, 6000.0.37f1) (push) Has been cancelled
Build project / Build for (StandaloneWindows64, 6000.0.37f1) (pull_request) Has been cancelled
Build project / Publish to itch.io (StandaloneLinux64) (pull_request) Has been cancelled
Build project / Build for (StandaloneLinux64, 6000.0.37f1) (pull_request) Has been cancelled
Build project / Build for (StandaloneWindows64, 6000.0.37f1) (push) Has been cancelled
Build project / Publish to itch.io (StandaloneLinux64) (push) Has been cancelled
Build project / Publish to itch.io (StandaloneWindows64) (push) Has been cancelled
Build project / Publish to itch.io (StandaloneWindows64) (pull_request) Has been cancelled
fix untangle and make sure full game works
2025-04-20 23:06:02 -04:00

274 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Point> points = new List<Point>();
public List<Connection> connections = new List<Connection>();
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<Point>();
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 Lineintersection 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
}