geometry-tower-defense/Assets/Tests/EditMode/InventoryGenerationStabilit...

664 lines
23 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 DropPoolRoller_Is_Reproducible_When_DropPool_Row_Order_Changes()
{
DROutGameDropPool whiteRow = CreateDropPoolRow(1, "MuzzleComp", 1, "[100,0,0,0,0]");
DROutGameDropPool greenRow = CreateDropPoolRow(2, "BearingComp", 1, "[0,100,0,0,0]");
DROutGameDropPool blueRow = CreateDropPoolRow(3, "BaseComp", 1, "[0,0,100,0,0]");
DROutGameDropPool purpleRow = CreateDropPoolRow(4, "MuzzleComp", 2, "[0,0,0,100,0]");
DROutGameDropPool redRow = CreateDropPoolRow(5, "BearingComp", 2, "[0,0,0,0,100]");
DropPoolRoller forwardOrderRoller = new DropPoolRoller(
new InsertionOrderDataTable<DROutGameDropPool>(whiteRow, greenRow, blueRow, purpleRow, redRow));
DropPoolRoller reverseOrderRoller = new DropPoolRoller(
new InsertionOrderDataTable<DROutGameDropPool>(redRow, purpleRow, blueRow, greenRow, whiteRow));
for (int seed = 1; seed <= 64; seed++)
{
bool forwardRolled = forwardOrderRoller.TryRollRow(
8,
LevelThemeType.Plain,
new System.Random(seed),
out DROutGameDropPool forwardRow,
out RarityType forwardRarity);
bool reverseRolled = reverseOrderRoller.TryRollRow(
8,
LevelThemeType.Plain,
new System.Random(seed),
out DROutGameDropPool reverseRow,
out RarityType reverseRarity);
Assert.That(forwardRolled, Is.True, $"seed={seed}");
Assert.That(reverseRolled, Is.True, $"seed={seed}");
Assert.That(reverseRarity, Is.EqualTo(forwardRarity), $"seed={seed}");
Assert.That(reverseRow.Id, Is.EqualTo(forwardRow.Id), $"seed={seed}");
}
}
[Test]
public void RewardSelectFormUseCase_Uses_Stable_Order_And_Deterministic_Selection_Rotation()
{
RewardSelectFormUseCase useCase = new RewardSelectFormUseCase();
useCase.ConfigureRewardPool(
new[]
{
CreateRewardRawData(1, "一号"),
CreateRewardRawData(2, "二号"),
CreateRewardRawData(3, "三号"),
CreateRewardRawData(4, "四号")
},
displayCount: 3,
refreshCost: 0,
allowRotateOnce: true,
allowGiveUp: false);
RewardSelectFormRawData initialModel = useCase.CreateInitialModel();
RewardSelectFormRawData refreshedModel = useCase.TryRotateSelection();
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)
{
int nextDropOrdinal = 0;
EnemyDropResult result = _component.ResolveEnemyDrop(
new EnemyDropContext(CreateEnemyRow(), 8, LevelThemeType.Plain),
runSeed,
sequenceIndex,
ref nextDropOrdinal);
return BuildDropSignaturePart(result);
}
private string BuildRewardCandidateSignature(int runSeed, int sequenceIndex)
{
int nextRewardOrdinal = 0;
IReadOnlyList<TowerCompItemData> candidates = _component.BuildRewardCandidates(
8,
LevelThemeType.Plain,
3,
runSeed,
sequenceIndex,
ref nextRewardOrdinal);
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;
}
}
private sealed class InsertionOrderDataTable<TRow> : IDataTable<TRow> where TRow : class, IDataRow
{
private readonly List<TRow> _rows = new();
private readonly Dictionary<int, TRow> _rowsById = new();
public InsertionOrderDataTable(params TRow[] rows)
{
if (rows == null)
{
return;
}
for (int i = 0; i < rows.Length; i++)
{
TRow row = rows[i];
if (row == null)
{
continue;
}
_rows.Add(row);
_rowsById[row.Id] = row;
}
}
public string Name => typeof(TRow).Name;
public string FullName => typeof(TRow).FullName;
public Type Type => typeof(TRow);
public int Count => _rows.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;
}
for (int i = 0; i < _rows.Count; i++)
{
TRow row = _rows[i];
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;
}
for (int i = 0; i < _rows.Count; i++)
{
TRow row = _rows[i];
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(_rows);
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;
}
results.AddRange(_rows);
}
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)
{
if (!_rowsById.TryGetValue(id, out TRow row))
{
return false;
}
_rowsById.Remove(id);
return _rows.Remove(row);
}
public void RemoveAllDataRows()
{
_rows.Clear();
_rowsById.Clear();
}
public IEnumerator<TRow> GetEnumerator()
{
for (int i = 0; i < _rows.Count; i++)
{
yield return _rows[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private int[] GetOrderedIds()
{
int[] ids = _rowsById.Keys.ToArray();
Array.Sort(ids);
return ids;
}
}
}
}