438 lines
16 KiB
C#
438 lines
16 KiB
C#
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<InventoryGenerationComponent>();
|
|
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<string> signatures = new HashSet<string>();
|
|
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<string> signatures = new HashSet<string>();
|
|
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<string> signatures = new HashSet<string>();
|
|
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<GoodsItemRawData> 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<TowerCompItemData> 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<DRShopPrice>(
|
|
CreateShopPriceRow(1, RarityType.Blue, 30, 35),
|
|
CreateShopPriceRow(2, RarityType.Purple, 60, 70)));
|
|
SetPrivateField(component, "_dropPoolTable", new FakeDataTable<DROutGameDropPool>(
|
|
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<DRMuzzleComp>(
|
|
CreateMuzzleRow(1, "火焰枪口", "[Fire,Ice,Crit]"),
|
|
CreateMuzzleRow(2, "暴击枪口", "[Crit,Shatter,Execution]")));
|
|
SetPrivateField(component, "_bearingCompTable", new FakeDataTable<DRBearingComp>(
|
|
CreateBearingRow(1, "寒冰轴承", "[Ice,Shatter]"),
|
|
CreateBearingRow(2, "穿透轴承", "[Pierce,Crit]")));
|
|
SetPrivateField(component, "_baseCompTable", new FakeDataTable<DRBaseComp>(
|
|
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<TRow> : IDataTable<TRow> where TRow : class, IDataRow
|
|
{
|
|
private readonly Dictionary<int, TRow> _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<TRow> condition) => GetDataRow(condition) != null;
|
|
|
|
public TRow GetDataRow(int id) => _rowsById.TryGetValue(id, out TRow row) ? row : null;
|
|
|
|
public TRow GetDataRow(Predicate<TRow> 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<TRow> condition)
|
|
{
|
|
List<TRow> results = new();
|
|
GetDataRows(condition, results);
|
|
return results.ToArray();
|
|
}
|
|
|
|
public void GetDataRows(Predicate<TRow> condition, List<TRow> 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<TRow> comparison)
|
|
{
|
|
List<TRow> results = new();
|
|
GetDataRows(comparison, results);
|
|
return results.ToArray();
|
|
}
|
|
|
|
public void GetDataRows(Comparison<TRow> comparison, List<TRow> results)
|
|
{
|
|
results?.Clear();
|
|
if (results == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
results.AddRange(_rowsById.Values);
|
|
if (comparison != null)
|
|
{
|
|
results.Sort(comparison);
|
|
}
|
|
}
|
|
|
|
public TRow[] GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison)
|
|
{
|
|
List<TRow> results = new();
|
|
GetDataRows(condition, comparison, results);
|
|
return results.ToArray();
|
|
}
|
|
|
|
public void GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison, List<TRow> results)
|
|
{
|
|
GetDataRows(condition, results);
|
|
if (results != null && comparison != null)
|
|
{
|
|
results.Sort(comparison);
|
|
}
|
|
}
|
|
|
|
public TRow[] GetAllDataRows()
|
|
{
|
|
List<TRow> results = new();
|
|
GetAllDataRows(results);
|
|
return results.ToArray();
|
|
}
|
|
|
|
public void GetAllDataRows(List<TRow> 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<TRow> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|