181 lines
7.1 KiB
C#
181 lines
7.1 KiB
C#
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<CandidateDatabase>(DatabasePath);
|
|
Assert.IsNotNull(db, $"Expected CandidateDatabase at {DatabasePath}");
|
|
return db;
|
|
}
|
|
|
|
SkillDatabase LoadSkillDatabase()
|
|
{
|
|
var db = AssetDatabase.LoadAssetAtPath<SkillDatabase>(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<string>(StringComparer.OrdinalIgnoreCase);
|
|
var seenClues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var axesUsed = new HashSet<SkillAxis>();
|
|
|
|
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<CandidateEvidenceType, int>();
|
|
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($"<<declare {clue.UnlockFlag}"),
|
|
$"Clue '{clue.Id}' unlockFlag '{clue.UnlockFlag}' is not declared in Common.yarn.");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Registry_NoCandidateUnlockFlagSetIsStrictSupersetOfAnother()
|
|
{
|
|
// Proxy, not a proof: if candidate A's unlock flags are a strict superset of B's,
|
|
// A is reliably discoverable only after B. Genuine order coverage is a playtest question.
|
|
var db = LoadDatabase();
|
|
var flagSets = new List<(string id, HashSet<string> flags)>();
|
|
|
|
foreach (var candidate in db.All)
|
|
{
|
|
var flags = new HashSet<string>(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.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|