Files
YachtDice/Assets/Scripts/Tests/Editor/ModifierPipelineTests.cs
T

398 lines
17 KiB
C#

using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using YachtDice.Categories;
using YachtDice.Modifiers.Conditions;
using YachtDice.Modifiers.Core;
using YachtDice.Modifiers.Definition;
using YachtDice.Modifiers.Effects;
using YachtDice.Modifiers.Pipeline;
using YachtDice.Modifiers.Runtime;
using YachtDice.Scoring;
namespace YachtDice.Tests
{
public class ModifierPipelineTests
{
private ModifierRegistry _registry;
private ModifierPipeline _pipeline;
// Тестовые категории
private CategoryDefinition _chanceCategory;
private CategoryDefinition _fullHouseCategory;
private CategoryDefinition _onesCategory;
private CategoryDefinition _threesCategory;
private CategoryDefinition _foursCategory;
private CategoryDefinition _sixesCategory;
[SetUp]
public void SetUp()
{
_registry = new ModifierRegistry(10);
_pipeline = new ModifierPipeline(_registry);
_pipeline.TracingEnabled = false;
_chanceCategory = SumAllCategory.CreateForTest("chance", "Шанс");
_fullHouseCategory = FullHouseCategory.CreateForTest("full_house", "Фулл-хаус");
_onesCategory = SumOfValueCategory.CreateForTest("ones", "Единицы", 1);
_threesCategory = SumOfValueCategory.CreateForTest("threes", "Тройки", 3);
_foursCategory = SumOfValueCategory.CreateForTest("fours", "Четвёрки", 4);
_sixesCategory = SumOfValueCategory.CreateForTest("sixes", "Шестёрки", 6);
}
[TearDown]
public void TearDown()
{
Object.DestroyImmediate(_chanceCategory);
Object.DestroyImmediate(_fullHouseCategory);
Object.DestroyImmediate(_onesCategory);
Object.DestroyImmediate(_threesCategory);
Object.DestroyImmediate(_foursCategory);
Object.DestroyImmediate(_sixesCategory);
}
private ModifierDefinition CreateDef(string id,
TriggerType trigger,
List<Condition> conditions,
List<Effect> effects)
{
var behavior = ModifierBehavior.CreateForTest(trigger, conditions, effects);
return ModifierDefinition.CreateForTest(id,
new List<ModifierBehavior> { behavior });
}
private void RegisterAndActivate(ModifierDefinition def)
{
var inst = _registry.Add(def);
_registry.TryActivate(inst);
}
private ModifierContext CreateScoringContext(int baseScore, int[] dice, CategoryDefinition category)
{
return new ModifierContext
{
BaseScore = baseScore,
DiceValues = dice,
Category = category,
AllActiveModifiers = _registry.Active,
};
}
// ── Phase Ordering ──────────────────────────────────────────
/// <summary>
/// Аддитивные эффекты применяются раньше мультипликативных.
/// </summary>
[Test]
public void Execute_AdditiveBeforeMultiplicative()
{
var addEffect = AddFlatScoreEffect.CreateForTest(10, ModifierPhase.Additive);
var mulEffect = MultiplyScoreEffect.CreateForTest(2f, ModifierPhase.Multiplicative);
var addDef = CreateDef("add", TriggerType.OnCategoryScored, null,
new List<Effect> { addEffect });
var mulDef = CreateDef("mul", TriggerType.OnCategoryScored, null,
new List<Effect> { mulEffect });
RegisterAndActivate(mulDef);
RegisterAndActivate(addDef);
var ctx = CreateScoringContext(20, new[] { 1, 2, 3, 4, 5 }, _chanceCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
// (20 + 10) * 2 = 60
Assert.AreEqual(60, result.FinalScore);
}
/// <summary>
/// Пост-мультипликативная фаза выполняется после мультипликативной.
/// </summary>
[Test]
public void Execute_PostMultiplicativeAfterMultiplicative()
{
var mulEffect = MultiplyScoreEffect.CreateForTest(2f, ModifierPhase.Multiplicative);
var postMulEffect = PostMultiplyEffect.CreateForTest(3f, ModifierPhase.PostMultiplicative);
var mulDef = CreateDef("mul", TriggerType.OnCategoryScored, null,
new List<Effect> { mulEffect });
var postDef = CreateDef("post", TriggerType.OnCategoryScored, null,
new List<Effect> { postMulEffect });
RegisterAndActivate(postDef);
RegisterAndActivate(mulDef);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, _chanceCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
// (10 + 0) * 2 * 3 = 60
Assert.AreEqual(60, result.FinalScore);
}
// ── Condition Filtering ─────────────────────────────────────
/// <summary>
/// Эффект пропускается, если условие модификатора не выполнено.
/// </summary>
[Test]
public void Execute_ConditionFails_SkipsEffect()
{
var condition = CategoryCondition.CreateForTest(_fullHouseCategory);
var effect = AddFlatScoreEffect.CreateForTest(100);
var def = CreateDef("fh-bonus", TriggerType.OnCategoryScored,
new List<Condition> { condition },
new List<Effect> { effect });
RegisterAndActivate(def);
// Scoring Ones, not FullHouse — condition should fail
var ctx = CreateScoringContext(5, new[] { 1, 1, 1, 1, 1 }, _onesCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(0, result.FlatBonus);
Assert.AreEqual(5, result.FinalScore);
}
/// <summary>
/// Эффект применяется, когда условие модификатора выполняется.
/// </summary>
[Test]
public void Execute_ConditionPasses_AppliesEffect()
{
var condition = CategoryCondition.CreateForTest(_fullHouseCategory);
var effect = AddFlatScoreEffect.CreateForTest(15);
var def = CreateDef("fh-bonus", TriggerType.OnCategoryScored,
new List<Condition> { condition },
new List<Effect> { effect });
RegisterAndActivate(def);
var ctx = CreateScoringContext(25, new[] { 3, 3, 3, 2, 2 }, _fullHouseCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(15, result.FlatBonus);
Assert.AreEqual(40, result.FinalScore);
}
// ── Trigger Filtering ───────────────────────────────────────
/// <summary>
/// Модификатор пропускается при несовпадении триггера события.
/// </summary>
[Test]
public void Execute_WrongTrigger_SkipsModifier()
{
var effect = AddFlatScoreEffect.CreateForTest(999);
var def = CreateDef("turn-bonus", TriggerType.OnTurnStart, null,
new List<Effect> { effect });
RegisterAndActivate(def);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, _chanceCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(0, result.FlatBonus);
Assert.AreEqual(10, result.FinalScore);
}
// ── Multiple Modifiers ──────────────────────────────────────
/// <summary>
/// Несколько модификаторов применяются в правильном порядке фаз и приоритетов.
/// </summary>
[Test]
public void Execute_MultipleModifiers_CorrectOrder()
{
var perDieAdd = AddPerDiceEffect.CreateForTest(2, targetDiceValue: 3, phase: ModifierPhase.Additive);
var perDieMul = MultiplyPerDiceEffect.CreateForTest(1.5f, targetDiceValue: 3, phase: ModifierPhase.Multiplicative);
var flatAdd = AddFlatScoreEffect.CreateForTest(10, ModifierPhase.Additive, priority: 10);
var finalMul = MultiplyScoreEffect.CreateForTest(2f, ModifierPhase.Multiplicative, priority: 10);
var def1 = CreateDef("pda", TriggerType.OnCategoryScored, null,
new List<Effect> { perDieAdd });
var def2 = CreateDef("pdm", TriggerType.OnCategoryScored, null,
new List<Effect> { perDieMul });
var def3 = CreateDef("fa", TriggerType.OnCategoryScored, null,
new List<Effect> { flatAdd });
var def4 = CreateDef("fm", TriggerType.OnCategoryScored, null,
new List<Effect> { finalMul });
RegisterAndActivate(def4);
RegisterAndActivate(def3);
RegisterAndActivate(def2);
RegisterAndActivate(def1);
// dice: [3, 3, 3, 1, 2] — 3 threes
var ctx = CreateScoringContext(9, new[] { 3, 3, 3, 1, 2 }, _threesCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(16, result.FlatBonus);
Assert.AreEqual(168, result.FinalScore);
}
// ── Empty / Null Cases ──────────────────────────────────────
/// <summary>
/// При отсутствии активных модификаторов результат не изменяется.
/// </summary>
[Test]
public void Execute_NoActiveModifiers_NoChange()
{
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, _chanceCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(10, result.FinalScore);
Assert.AreEqual(0, result.FlatBonus);
Assert.AreEqual(1f, result.Multiplier);
}
/// <summary>
/// Неактивный модификатор не влияет на вычисление результата.
/// </summary>
[Test]
public void Execute_InactiveModifier_Skipped()
{
var effect = AddFlatScoreEffect.CreateForTest(50);
var def = CreateDef("inactive", TriggerType.OnCategoryScored, null,
new List<Effect> { effect });
_registry.Add(def);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, _chanceCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(0, result.FlatBonus);
Assert.AreEqual(10, result.FinalScore);
}
// ── Side Effects ────────────────────────────────────────────
/// <summary>
/// Побочные эффекты выполняются в фазе SideEffect и сохраняют корректный результат.
/// </summary>
[Test]
public void Execute_SideEffectsInSideEffectPhase()
{
var scoreEffect = AddFlatScoreEffect.CreateForTest(10, ModifierPhase.Additive);
var currencyEffect = AddCurrencyEffect.CreateForTest(25, ModifierPhase.SideEffect);
var def = CreateDef("rewards", TriggerType.OnCategoryScored, null,
new List<Effect> { scoreEffect, currencyEffect });
RegisterAndActivate(def);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, _chanceCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(10, result.FlatBonus);
Assert.AreEqual(25, result.CurrencyDelta);
Assert.AreEqual(20, result.FinalScore);
}
// ── Tracing ─────────────────────────────────────────────────
/// <summary>
/// При включенном трейсинге pipeline наполняет отладочный лог.
/// </summary>
[Test]
public void Execute_TracingEnabled_PopulatesDebugLog()
{
_pipeline.TracingEnabled = true;
var effect = AddFlatScoreEffect.CreateForTest(10);
var def = CreateDef("traced", TriggerType.OnCategoryScored, null,
new List<Effect> { effect });
RegisterAndActivate(def);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, _chanceCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.IsNotNull(result.DebugLog);
Assert.IsTrue(result.DebugLog.Count > 0);
}
// ── DiceValue Condition ──────────────────────────────────────
/// <summary>
/// Условие по значению костей срабатывает только при достижении нужного количества совпадений.
/// </summary>
[Test]
public void Execute_DiceValueCondition_OnlyTriggersOnMatch()
{
var condition = DiceValueCondition.CreateForTest(6, minCount: 3);
var effect = AddFlatScoreEffect.CreateForTest(100);
var def = CreateDef("sixes-bonus", TriggerType.OnCategoryScored,
new List<Condition> { condition },
new List<Effect> { effect });
RegisterAndActivate(def);
// Only 2 sixes — condition requires 3
var ctx = CreateScoringContext(12, new[] { 6, 6, 1, 2, 3 }, _sixesCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(0, result.FlatBonus);
// 3 sixes — condition passes
var ctx2 = CreateScoringContext(18, new[] { 6, 6, 6, 1, 2 }, _sixesCategory);
var result2 = _pipeline.Execute(TriggerType.OnCategoryScored, ctx2).GetAwaiter().GetResult();
Assert.AreEqual(100, result2.FlatBonus);
}
// ── MinScore Condition ──────────────────────────────────────
/// <summary>
/// Условие минимального базового счета работает ниже и на пороговом значении.
/// </summary>
[Test]
public void Execute_MinScoreCondition_ThresholdWorks()
{
var condition = MinScoreCondition.CreateForTest(20);
var effect = MultiplyScoreEffect.CreateForTest(2f);
var def = CreateDef("high-score-bonus", TriggerType.OnCategoryScored,
new List<Condition> { condition },
new List<Effect> { effect });
RegisterAndActivate(def);
// Below threshold
var ctx = CreateScoringContext(15, new[] { 3, 3, 3, 3, 3 }, _threesCategory);
var result = _pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.AreEqual(1f, result.Multiplier);
// At threshold
var ctx2 = CreateScoringContext(20, new[] { 4, 4, 4, 4, 4 }, _foursCategory);
var result2 = _pipeline.Execute(TriggerType.OnCategoryScored, ctx2).GetAwaiter().GetResult();
Assert.AreEqual(2f, result2.Multiplier);
}
// ── ToScoreResult ───────────────────────────────────────────
/// <summary>
/// Преобразование контекста в ScoreResult корректно переносит все поля.
/// </summary>
[Test]
public void ToScoreResult_ConvertsCorrectly()
{
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, _chanceCategory);
ctx.FlatBonus = 5;
ctx.Multiplier = 2f;
ctx.PostMultiplier = 1.5f;
ScoreResult sr = ctx.ToScoreResult();
Assert.AreEqual(10, sr.baseScore);
Assert.AreEqual(5, sr.flatBonus);
Assert.AreEqual(3f, sr.multiplier, 0.001f); // 2 * 1.5
Assert.AreEqual(_chanceCategory, sr.category);
}
}
}