using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using GameFramework.DataTable; using GeometryTD.CustomComponent; using GeometryTD.DataTable; using GeometryTD.Definition; using GeometryTD.UI; using NUnit.Framework; using UnityEngine; using Object = UnityEngine.Object; namespace GeometryTD.Tests.EditMode { public sealed class InventoryGenerationStabilityTests { private GameObject _gameObject; private InventoryGenerationComponent _component; [SetUp] public void SetUp() { TagDefinitionRegistry.ResetToDefaults(); TagGenerationRuleRegistry.ResetToDefaults(); RarityTagBudgetRuleRegistry.ResetToDefaults(); _gameObject = new GameObject("InventoryGenerationStabilityTests"); _component = _gameObject.AddComponent(); BindTables(_component); } [TearDown] public void TearDown() { TagDefinitionRegistry.ResetToDefaults(); TagGenerationRuleRegistry.ResetToDefaults(); RarityTagBudgetRuleRegistry.ResetToDefaults(); if (_gameObject != null) { Object.DestroyImmediate(_gameObject); _gameObject = null; _component = null; } } [Test] public void BuildShopGoods_Is_Reproducible_For_Same_RunSeed_And_SequenceIndex() { string first = BuildShopSignature(runSeed: 1001, sequenceIndex: 3); string second = BuildShopSignature(runSeed: 1001, sequenceIndex: 3); Assert.That(second, Is.EqualTo(first)); } [Test] public void BuildShopGoods_Distinguishes_Different_RunSeed() { HashSet signatures = new HashSet(); for (int runSeed = 1001; runSeed < 1017; runSeed++) { signatures.Add(BuildShopSignature(runSeed, sequenceIndex: 3)); } Assert.That(signatures.Count, Is.GreaterThan(1)); } [Test] public void ResolveEnemyDrop_Is_Reproducible_For_Same_RunSeed_And_SequenceIndex() { string first = BuildEnemyDropSignature(runSeed: 2001, sequenceIndex: 5); string second = BuildEnemyDropSignature(runSeed: 2001, sequenceIndex: 5); Assert.That(second, Is.EqualTo(first)); } [Test] public void ResolveEnemyDrop_Distinguishes_Different_RunSeed() { HashSet signatures = new HashSet(); for (int runSeed = 2001; runSeed < 2033; runSeed++) { signatures.Add(BuildEnemyDropSignature(runSeed, sequenceIndex: 5)); } Assert.That(signatures.Count, Is.GreaterThan(1)); } [Test] public void BuildRewardCandidates_Is_Reproducible_For_Same_RunSeed_And_SequenceIndex() { string first = BuildRewardCandidateSignature(runSeed: 3001, sequenceIndex: 7); string second = BuildRewardCandidateSignature(runSeed: 3001, sequenceIndex: 7); Assert.That(second, Is.EqualTo(first)); } [Test] public void BuildRewardCandidates_Distinguishes_Different_RunSeed() { HashSet signatures = new HashSet(); for (int runSeed = 3001; runSeed < 3017; runSeed++) { signatures.Add(BuildRewardCandidateSignature(runSeed, sequenceIndex: 7)); } Assert.That(signatures.Count, Is.GreaterThan(1)); } [Test] public void RewardSelectFormUseCase_Uses_Stable_Order_And_Deterministic_Refresh_Rotation() { RewardSelectFormUseCase useCase = new RewardSelectFormUseCase(); useCase.ConfigureRewardPool( new[] { CreateRewardRawData(1, "一号"), CreateRewardRawData(2, "二号"), CreateRewardRawData(3, "三号"), CreateRewardRawData(4, "四号") }, displayCount: 3, refreshCost: 0, allowRefreshOnce: true, allowGiveUp: false); RewardSelectFormRawData initialModel = useCase.CreateInitialModel(); RewardSelectFormRawData refreshedModel = useCase.TryRefresh(); Assert.That(initialModel.RewardItems.Select(item => item.Title).ToArray(), Is.EqualTo(new[] { "一号", "二号", "三号" })); Assert.That(refreshedModel.RewardItems.Select(item => item.Title).ToArray(), Is.EqualTo(new[] { "四号", "一号", "二号" })); } private string BuildShopSignature(int runSeed, int sequenceIndex) { List goods = _component.BuildShopGoods(4, runSeed, sequenceIndex); return string.Join("|", goods.Select(BuildGoodsSignaturePart)); } private string BuildEnemyDropSignature(int runSeed, int sequenceIndex) { _component.ConfigureRunContext(runSeed, sequenceIndex); EnemyDropResult result = _component.ResolveEnemyDrop(new EnemyDropContext(CreateEnemyRow(), 8, LevelThemeType.Plain)); return BuildDropSignaturePart(result); } private string BuildRewardCandidateSignature(int runSeed, int sequenceIndex) { _component.ConfigureRunContext(runSeed, sequenceIndex); IReadOnlyList candidates = _component.BuildRewardCandidates(8, LevelThemeType.Plain, 3); return string.Join("|", candidates.Select(BuildItemSignaturePart)); } private static string BuildGoodsSignaturePart(GoodsItemRawData goods) { return $"{goods.GoodsIndex}:{goods.Price}:{BuildItemSignaturePart(goods.SourceItem)}"; } private static string BuildDropSignaturePart(EnemyDropResult result) { return $"{result.Coin}:{result.Gold}:{BuildItemSignaturePart(result.LootItem)}"; } private static string BuildItemSignaturePart(TowerCompItemData item) { if (item == null) { return "null"; } string tags = item.Tags == null || item.Tags.Length <= 0 ? string.Empty : string.Join(",", item.Tags.Select(tag => tag.ToString())); return $"{item.InstanceId}:{item.SlotType}:{item.ConfigId}:{item.Name}:{item.Rarity}:{tags}"; } private static RewardSelectItemRawData CreateRewardRawData(long instanceId, string title) { return new RewardSelectItemRawData { Title = title, SlotType = TowerCompSlotType.Muzzle, SourceItem = new MuzzleCompItemData { InstanceId = instanceId, Name = title, } }; } private static void BindTables(InventoryGenerationComponent component) { SetPrivateField(component, "_shopPriceTable", new FakeDataTable( CreateShopPriceRow(1, RarityType.Blue, 30, 35), CreateShopPriceRow(2, RarityType.Purple, 60, 70))); SetPrivateField(component, "_dropPoolTable", new FakeDataTable( CreateDropPoolRow(1, "MuzzleComp", 1, "[50,40,30,10,0]"), CreateDropPoolRow(2, "MuzzleComp", 2, "[10,30,50,20,0]"), CreateDropPoolRow(3, "BearingComp", 1, "[20,40,20,10,0]"), CreateDropPoolRow(4, "BaseComp", 1, "[20,20,40,10,0]"))); SetPrivateField(component, "_muzzleCompTable", new FakeDataTable( CreateMuzzleRow(1, "火焰枪口", "[Fire,Ice,Crit]"), CreateMuzzleRow(2, "暴击枪口", "[Crit,Shatter,Execution]"))); SetPrivateField(component, "_bearingCompTable", new FakeDataTable( CreateBearingRow(1, "寒冰轴承", "[Ice,Shatter]"), CreateBearingRow(2, "穿透轴承", "[Pierce,Crit]"))); SetPrivateField(component, "_baseCompTable", new FakeDataTable( CreateBaseRow(1, "迅捷底座", "[Fire,Crit]"), CreateBaseRow(2, "处决底座", "[Execution,Ice]"))); } private static void SetPrivateField(object instance, string fieldName, object value) { FieldInfo field = instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(field, Is.Not.Null, fieldName); field.SetValue(instance, value); } private static DRShopPrice CreateShopPriceRow(int id, RarityType rarity, int minPrice, int maxPrice) { DRShopPrice row = new DRShopPrice(); Assert.That(row.ParseDataRow($"\t{id}\t\t{rarity}\t{minPrice}\t{maxPrice}", null), Is.True); return row; } private static DROutGameDropPool CreateDropPoolRow(int id, string itemType, int itemId, string weights) { DROutGameDropPool row = new DROutGameDropPool(); Assert.That( row.ParseDataRow($"\t{id}\t\tPlain\t{itemType}\t{itemId}\t{weights}\t[1,1,1,1,1]\t[99,99,99,99,99]", null), Is.True); return row; } private static DRMuzzleComp CreateMuzzleRow(int id, string name, string possibleTags) { DRMuzzleComp row = new DRMuzzleComp(); Assert.That( row.ParseDataRow($"\t{id}\t\t{name}\t[10,20,30,40,50]\t3\t0.15\tNormalBullet\t\t{possibleTags}", null), Is.True); return row; } private static DRBearingComp CreateBearingRow(int id, string name, string possibleTags) { DRBearingComp row = new DRBearingComp(); Assert.That( row.ParseDataRow($"\t{id}\t\t{name}\t[1,2,3,4,5]\t0.5\t[10,20,30,40,50]\t1\t\t{possibleTags}", null), Is.True); return row; } private static DRBaseComp CreateBaseRow(int id, string name, string possibleTags) { DRBaseComp row = new DRBaseComp(); Assert.That( row.ParseDataRow($"\t{id}\t\t{name}\t[2,4,6,8,10]\t-0.25\tFire\t\t{possibleTags}", null), Is.True); return row; } private static DREnemy CreateEnemyRow() { DREnemy row = new DREnemy(); Assert.That(row.ParseDataRow("\t1\t\t1\t100\t1\t1\t3\t5\t1", null), Is.True); return row; } private sealed class FakeDataTable : IDataTable where TRow : class, IDataRow { private readonly Dictionary _rowsById = new(); public FakeDataTable(params TRow[] rows) { if (rows == null) { return; } for (int i = 0; i < rows.Length; i++) { TRow row = rows[i]; if (row != null) { _rowsById[row.Id] = row; } } } public string Name => typeof(TRow).Name; public string FullName => typeof(TRow).FullName; public Type Type => typeof(TRow); public int Count => _rowsById.Count; public TRow this[int id] => GetDataRow(id); public TRow MinIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[0]); public TRow MaxIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[^1]); public bool HasDataRow(int id) => _rowsById.ContainsKey(id); public bool HasDataRow(Predicate condition) => GetDataRow(condition) != null; public TRow GetDataRow(int id) => _rowsById.TryGetValue(id, out TRow row) ? row : null; public TRow GetDataRow(Predicate condition) { if (condition == null) { return null; } foreach (TRow row in _rowsById.Values) { if (row != null && condition(row)) { return row; } } return null; } public TRow[] GetDataRows(Predicate condition) { List results = new(); GetDataRows(condition, results); return results.ToArray(); } public void GetDataRows(Predicate condition, List results) { results?.Clear(); if (condition == null || results == null) { return; } foreach (TRow row in _rowsById.Values) { if (row != null && condition(row)) { results.Add(row); } } } public TRow[] GetDataRows(Comparison comparison) { List results = new(); GetDataRows(comparison, results); return results.ToArray(); } public void GetDataRows(Comparison comparison, List results) { results?.Clear(); if (results == null) { return; } results.AddRange(_rowsById.Values); if (comparison != null) { results.Sort(comparison); } } public TRow[] GetDataRows(Predicate condition, Comparison comparison) { List results = new(); GetDataRows(condition, comparison, results); return results.ToArray(); } public void GetDataRows(Predicate condition, Comparison comparison, List results) { GetDataRows(condition, results); if (results != null && comparison != null) { results.Sort(comparison); } } public TRow[] GetAllDataRows() { List results = new(); GetAllDataRows(results); return results.ToArray(); } public void GetAllDataRows(List results) { results?.Clear(); if (results == null) { return; } foreach (int id in GetOrderedIds()) { results.Add(_rowsById[id]); } } public bool AddDataRow(string dataRowString, object userData) => throw new NotSupportedException(); public bool AddDataRow(byte[] dataRowBytes, int startIndex, int length, object userData) => throw new NotSupportedException(); public bool RemoveDataRow(int id) => _rowsById.Remove(id); public void RemoveAllDataRows() { _rowsById.Clear(); } public IEnumerator GetEnumerator() { foreach (int id in GetOrderedIds()) { yield return _rowsById[id]; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } private int[] GetOrderedIds() { int[] ids = _rowsById.Keys.ToArray(); Array.Sort(ids); return ids; } } } }