using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using NUnit.Framework; using NightclubArcadia.Skills; using UnityEditor; using UnityEngine; namespace NightclubArcadia.Skills.Tests { public class CandidateRegistryTests { const string DatabasePath = "Assets/Candidates/CandidateDatabase.asset"; const string SkillDatabasePath = "Assets/Skills/SkillDatabase.asset"; const string CommonYarnPath = "Assets/Dialogue/Common.yarn"; static readonly Regex IdPattern = new Regex(@"^[a-z][a-z0-9_]*$"); static readonly Regex UnlockFlagPattern = new Regex(@"^\$(visited|errand)_[a-z][a-z0-9_]*$"); CandidateDatabase LoadDatabase() { // Assets are checked in (generated by CandidateSystemSetup). Same pattern as // SkillRosterTests — the test assembly cannot reference the Editor setup type. var db = AssetDatabase.LoadAssetAtPath(DatabasePath); Assert.IsNotNull(db, $"Expected CandidateDatabase at {DatabasePath}"); return db; } SkillDatabase LoadSkillDatabase() { var db = AssetDatabase.LoadAssetAtPath(SkillDatabasePath); Assert.IsNotNull(db, $"Expected SkillDatabase at {SkillDatabasePath}"); return db; } [Test] public void Registry_HasSymmetricClues_UniqueNonBodyIntuitionAxes_AndValidIds() { var db = LoadDatabase(); var skills = LoadSkillDatabase(); Assert.GreaterOrEqual(db.All.Count, 1, "Registry must contain at least one candidate."); var seenCandidates = new HashSet(StringComparer.OrdinalIgnoreCase); var seenClues = new HashSet(StringComparer.OrdinalIgnoreCase); var axesUsed = new HashSet(); foreach (var candidate in db.All) { Assert.IsNotNull(candidate); Assert.IsTrue(IdPattern.IsMatch(candidate.Id), $"Bad candidate id '{candidate.Id}'"); Assert.IsTrue(seenCandidates.Add(candidate.Id), $"Duplicate candidate id '{candidate.Id}'"); Assert.AreNotEqual( SkillAxis.Body, candidate.IntuitionAxis, $"Candidate '{candidate.Id}' must not use Body as intuitionAxis."); Assert.AreNotEqual( SkillAxis.Specialist, candidate.IntuitionAxis, $"Candidate '{candidate.Id}' must not use Specialist as intuitionAxis."); Assert.IsTrue( axesUsed.Add(candidate.IntuitionAxis), $"intuitionAxis {candidate.IntuitionAxis} shared by more than one candidate."); Assert.IsNotNull(candidate.Clues); var typeCounts = new Dictionary(); foreach (var clue in candidate.Clues) { Assert.IsNotNull(clue); Assert.IsTrue(IdPattern.IsMatch(clue.Id), $"Bad clue id '{clue.Id}'"); Assert.IsTrue(seenClues.Add(clue.Id), $"Duplicate clue id '{clue.Id}'"); if (!typeCounts.ContainsKey(clue.Type)) { typeCounts[clue.Type] = 0; } typeCounts[clue.Type]++; if (clue.Type == CandidateEvidenceType.Intuition) { Assert.IsFalse(string.IsNullOrEmpty(clue.SkillId), $"Intuition clue '{clue.Id}' needs skillId."); Assert.IsTrue( skills.TryGet(clue.SkillId, out var skill), $"Intuition clue '{clue.Id}' skillId '{clue.SkillId}' missing from SkillDatabase."); Assert.AreEqual( candidate.IntuitionAxis, skill.Axis, $"Intuition clue '{clue.Id}' skill axis mismatch."); } } foreach (CandidateEvidenceType type in Enum.GetValues(typeof(CandidateEvidenceType))) { typeCounts.TryGetValue(type, out var count); Assert.AreEqual( 1, count, $"Candidate '{candidate.Id}' must have exactly one {type} clue."); } } } [Test] public void Registry_UnlockFlags_AreExplorationGated_AndDeclared() { var db = LoadDatabase(); var commonYarn = File.ReadAllText(CommonYarnPath); foreach (var candidate in db.All) { foreach (var clue in candidate.Clues) { if (string.IsNullOrEmpty(clue.UnlockFlag)) { continue; } Assert.IsTrue( UnlockFlagPattern.IsMatch(clue.UnlockFlag), $"Clue '{clue.Id}' unlockFlag '{clue.UnlockFlag}' must match " + @"^\$(visited|errand)_[a-z][a-z0-9_]*$."); Assert.IsTrue( commonYarn.Contains($"< flags)>(); foreach (var candidate in db.All) { var flags = new HashSet(StringComparer.Ordinal); foreach (var clue in candidate.Clues) { if (!string.IsNullOrEmpty(clue.UnlockFlag)) { flags.Add(clue.UnlockFlag); } } flagSets.Add((candidate.Id, flags)); } for (var i = 0; i < flagSets.Count; i++) { for (var j = 0; j < flagSets.Count; j++) { if (i == j) { continue; } var a = flagSets[i]; var b = flagSets[j]; if (a.flags.Count <= b.flags.Count) { continue; } Assert.IsFalse( b.flags.IsSubsetOf(a.flags) && !a.flags.SetEquals(b.flags), $"Candidate '{a.id}' unlock-flag set is a strict superset of '{b.id}' — " + "that makes discovery order reliably stacked."); } } } } }