StellarXipher/Assets/Puzzles/Untangle/Scripts/UntangleGameManager.cs
EthanPisani 3b12b691cf
Some checks failed
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 / Build for (StandaloneLinux64, 6000.0.37f1) (push) Has been cancelled
add most of untangle game
2025-04-20 19:28:52 -04:00

271 lines
7.7 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()
{
// 1) Cleanup previous run
ClearOldGame();
points.Clear();
connections.Clear();
statusText.text = "";
if (winUI != null)
winUI.SetActive(false);
if (loseUI != null)
loseUI.SetActive(false);
// 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;
}
}
}
// set state to playing
}
// 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();
SetGameState(UGameState.Playing);
}
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);
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
}