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
271 lines
7.7 KiB
C#
271 lines
7.7 KiB
C#
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 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
|
||
}
|