[Refactor] Replace hardcoded categories with data-driven SO system and abstract dice

- Add abstract dice system (IDie interface, DieDefinitionSO, StandardDieSO, DieInstance)
  to support future custom dice types while keeping backward compat via int[] DiceValues
- Replace YachtCategory enum and CategoryScorer switch with CategoryDefinitionSO hierarchy:
  SumOfValueCategorySO, NOfAKindCategorySO, FullHouseCategorySO, StraightCategorySO, SumAllCategorySO
- Add CategoryCatalogSO for ordered category collections and DiceCheckUtility for shared logic
- Refactor ScoringSystem, Views, GameManager, GameController to use SO references
- Update CategoryCondition modifier to use SO reference instead of enum
- Update all editor tests to use SO-based categories and DieInstance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 11:46:50 +07:00
parent 6a48d68f75
commit 0f9b162061
31 changed files with 845 additions and 298 deletions
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using YachtDice.Categories;
using YachtDice.Modifiers.Conditions;
using YachtDice.Modifiers.Core;
using YachtDice.Modifiers.Definition;
@@ -16,12 +17,38 @@ namespace YachtDice.Tests
private ModifierRegistry registry;
private ModifierPipeline pipeline;
// Тестовые категории
private CategoryDefinitionSO chanceCategory;
private CategoryDefinitionSO fullHouseCategory;
private CategoryDefinitionSO onesCategory;
private CategoryDefinitionSO threesCategory;
private CategoryDefinitionSO foursCategory;
private CategoryDefinitionSO sixesCategory;
[SetUp]
public void SetUp()
{
registry = new ModifierRegistry(10);
pipeline = new ModifierPipeline(registry);
pipeline.TracingEnabled = false; // disable debug logs during tests
pipeline.TracingEnabled = false;
chanceCategory = SumAllCategorySO.CreateForTest("chance", "Шанс");
fullHouseCategory = FullHouseCategorySO.CreateForTest("full_house", "Фулл-хаус");
onesCategory = SumOfValueCategorySO.CreateForTest("ones", "Единицы", 1);
threesCategory = SumOfValueCategorySO.CreateForTest("threes", "Тройки", 3);
foursCategory = SumOfValueCategorySO.CreateForTest("fours", "Четвёрки", 4);
sixesCategory = SumOfValueCategorySO.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 ModifierDefinitionSO CreateDef(string id,
@@ -40,7 +67,7 @@ namespace YachtDice.Tests
registry.TryActivate(inst);
}
private ModifierContext CreateScoringContext(int baseScore, int[] dice, YachtCategory category)
private ModifierContext CreateScoringContext(int baseScore, int[] dice, CategoryDefinitionSO category)
{
return new ModifierContext
{
@@ -64,10 +91,10 @@ namespace YachtDice.Tests
var mulDef = CreateDef("mul", TriggerType.OnCategoryScored, null,
new List<EffectSO> { mulEffect });
RegisterAndActivate(mulDef); // registered first, but multiplicative phase
RegisterAndActivate(addDef); // registered second, but additive phase
RegisterAndActivate(mulDef);
RegisterAndActivate(addDef);
var ctx = CreateScoringContext(20, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance);
var ctx = CreateScoringContext(20, new[] { 1, 2, 3, 4, 5 }, chanceCategory);
var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
// (20 + 10) * 2 = 60
@@ -88,7 +115,7 @@ namespace YachtDice.Tests
RegisterAndActivate(postDef);
RegisterAndActivate(mulDef);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance);
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
@@ -100,7 +127,7 @@ namespace YachtDice.Tests
[Test]
public void Execute_ConditionFails_SkipsEffect()
{
var condition = CategoryCondition.CreateForTest(YachtCategory.FullHouse);
var condition = CategoryCondition.CreateForTest(fullHouseCategory);
var effect = AddFlatScoreEffect.CreateForTest(100);
var def = CreateDef("fh-bonus", TriggerType.OnCategoryScored,
@@ -110,7 +137,7 @@ namespace YachtDice.Tests
RegisterAndActivate(def);
// Scoring Ones, not FullHouse — condition should fail
var ctx = CreateScoringContext(5, new[] { 1, 1, 1, 1, 1 }, YachtCategory.Ones);
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);
@@ -120,7 +147,7 @@ namespace YachtDice.Tests
[Test]
public void Execute_ConditionPasses_AppliesEffect()
{
var condition = CategoryCondition.CreateForTest(YachtCategory.FullHouse);
var condition = CategoryCondition.CreateForTest(fullHouseCategory);
var effect = AddFlatScoreEffect.CreateForTest(15);
var def = CreateDef("fh-bonus", TriggerType.OnCategoryScored,
@@ -129,7 +156,7 @@ namespace YachtDice.Tests
RegisterAndActivate(def);
var ctx = CreateScoringContext(25, new[] { 3, 3, 3, 2, 2 }, YachtCategory.FullHouse);
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);
@@ -147,8 +174,7 @@ namespace YachtDice.Tests
RegisterAndActivate(def);
// Fire OnCategoryScored, not OnTurnStart
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance);
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);
@@ -180,12 +206,9 @@ namespace YachtDice.Tests
RegisterAndActivate(def1);
// dice: [3, 3, 3, 1, 2] — 3 threes
var ctx = CreateScoringContext(9, new[] { 3, 3, 3, 1, 2 }, YachtCategory.Threes);
var ctx = CreateScoringContext(9, new[] { 3, 3, 3, 1, 2 }, threesCategory);
var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
// Additive phase: perDieAdd (+2*3=6) + flatAdd (+10) → FlatBonus = 16
// Multiplicative phase: perDieMul (1.5^3=3.375) then finalMul (*2) → Multiplier = 6.75
// FinalScore = floor((9 + 16) * 6.75) = floor(168.75) = 168
Assert.AreEqual(16, result.FlatBonus);
Assert.AreEqual(168, result.FinalScore);
}
@@ -195,7 +218,7 @@ namespace YachtDice.Tests
[Test]
public void Execute_NoActiveModifiers_NoChange()
{
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance);
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);
@@ -210,10 +233,9 @@ namespace YachtDice.Tests
var def = CreateDef("inactive", TriggerType.OnCategoryScored, null,
new List<EffectSO> { effect });
// Add but don't activate
registry.Add(def);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance);
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);
@@ -233,7 +255,7 @@ namespace YachtDice.Tests
RegisterAndActivate(def);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance);
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);
@@ -254,7 +276,7 @@ namespace YachtDice.Tests
RegisterAndActivate(def);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory);
var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult();
Assert.IsNotNull(result.DebugLog);
@@ -276,13 +298,13 @@ namespace YachtDice.Tests
RegisterAndActivate(def);
// Only 2 sixes — condition requires 3
var ctx = CreateScoringContext(12, new[] { 6, 6, 1, 2, 3 }, YachtCategory.Sixes);
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 }, YachtCategory.Sixes);
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);
@@ -303,13 +325,13 @@ namespace YachtDice.Tests
RegisterAndActivate(def);
// Below threshold
var ctx = CreateScoringContext(15, new[] { 3, 3, 3, 3, 3 }, YachtCategory.Threes);
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 }, YachtCategory.Fours);
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);
@@ -320,7 +342,7 @@ namespace YachtDice.Tests
[Test]
public void ToScoreResult_ConvertsCorrectly()
{
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance);
var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory);
ctx.FlatBonus = 5;
ctx.Multiplier = 2f;
ctx.PostMultiplier = 1.5f;
@@ -330,7 +352,7 @@ namespace YachtDice.Tests
Assert.AreEqual(10, sr.BaseScore);
Assert.AreEqual(5, sr.FlatBonus);
Assert.AreEqual(3f, sr.Multiplier, 0.001f); // 2 * 1.5
Assert.AreEqual(YachtCategory.Chance, sr.Category);
Assert.AreEqual(chanceCategory, sr.Category);
}
}
}