InventoryGenerationComponent + TagRegistryComponent 收口
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3236620385724df2a9146f077f7413ae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user