[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:
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace YachtDice.Categories
|
||||
{
|
||||
/// <summary>
|
||||
/// Каталог всех доступных категорий.
|
||||
/// Порядок определяет порядок отображения в UI.
|
||||
/// Аналог ModifierCatalogSO.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "CategoryCatalog", menuName = "YachtDice/Categories/Catalog")]
|
||||
public class CategoryCatalogSO : ScriptableObject
|
||||
{
|
||||
[SerializeField] private List<CategoryDefinitionSO> categories = new();
|
||||
|
||||
public IReadOnlyList<CategoryDefinitionSO> All => categories;
|
||||
public int Count => categories.Count;
|
||||
|
||||
public CategoryDefinitionSO FindById(string id)
|
||||
{
|
||||
for (int i = 0; i < categories.Count; i++)
|
||||
{
|
||||
if (categories[i] != null && categories[i].Id == id)
|
||||
return categories[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int IndexOf(CategoryDefinitionSO def)
|
||||
{
|
||||
for (int i = 0; i < categories.Count; i++)
|
||||
{
|
||||
if (categories[i] == def)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static CategoryCatalogSO CreateForTest(List<CategoryDefinitionSO> defs)
|
||||
{
|
||||
var catalog = CreateInstance<CategoryCatalogSO>();
|
||||
catalog.categories = defs ?? new List<CategoryDefinitionSO>();
|
||||
return catalog;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using YachtDice.Dice;
|
||||
|
||||
namespace YachtDice.Categories
|
||||
{
|
||||
/// <summary>
|
||||
/// Абстрактное определение категории для скоринга.
|
||||
/// Каждая категория знает как вычислить очки по набору дайсов.
|
||||
/// </summary>
|
||||
public abstract class CategoryDefinitionSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
[SerializeField] private string id;
|
||||
[SerializeField] private string displayName;
|
||||
[SerializeField, TextArea] private string description;
|
||||
[SerializeField] private Sprite icon;
|
||||
|
||||
[Header("Section")]
|
||||
[SerializeField] private bool isUpperSection;
|
||||
|
||||
public string Id => id;
|
||||
public string DisplayName => displayName;
|
||||
public string Description => description;
|
||||
public Sprite Icon => icon;
|
||||
public bool IsUpperSection => isUpperSection;
|
||||
|
||||
/// <summary>
|
||||
/// Вычисляет очки для данного набора дайсов.
|
||||
/// </summary>
|
||||
public abstract int Calculate(IReadOnlyList<IDie> dice);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void SetTestData(string testId, string testDisplayName, bool upperSection = false)
|
||||
{
|
||||
id = testId;
|
||||
displayName = testDisplayName;
|
||||
isUpperSection = upperSection;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using YachtDice.Dice;
|
||||
|
||||
namespace YachtDice.Categories
|
||||
{
|
||||
/// <summary>
|
||||
/// Категория Фулл-хаус: 3 одинаковых + 2 одинаковых.
|
||||
/// При совпадении возвращает фиксированное число очков.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "FullHouseCategory", menuName = "YachtDice/Categories/Full House")]
|
||||
public class FullHouseCategorySO : CategoryDefinitionSO
|
||||
{
|
||||
[Header("Scoring")]
|
||||
[Tooltip("Фиксированное число очков за фулл-хаус")]
|
||||
[SerializeField] private int fixedScore = 25;
|
||||
|
||||
public override int Calculate(IReadOnlyList<IDie> dice)
|
||||
{
|
||||
int[] values = DiceCheckUtility.ExtractValues(dice);
|
||||
return DiceCheckUtility.IsFullHouse(values) ? fixedScore : 0;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static FullHouseCategorySO CreateForTest(string id, string displayName, int score = 25)
|
||||
{
|
||||
var so = CreateInstance<FullHouseCategorySO>();
|
||||
so.SetTestData(id, displayName);
|
||||
so.fixedScore = score;
|
||||
return so;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using YachtDice.Dice;
|
||||
|
||||
namespace YachtDice.Categories
|
||||
{
|
||||
/// <summary>
|
||||
/// Категория N-одинаковых: проверяет наличие N дайсов с одинаковым значением.
|
||||
/// При успехе возвращает сумму всех дайсов или фиксированное число очков.
|
||||
/// Используется для Тройки (3, сумма), Каре (4, сумма), Яхты (5, fixed=50).
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "NOfAKindCategory", menuName = "YachtDice/Categories/N Of A Kind")]
|
||||
public class NOfAKindCategorySO : CategoryDefinitionSO
|
||||
{
|
||||
[Header("Scoring")]
|
||||
[Tooltip("Сколько одинаковых дайсов требуется")]
|
||||
[SerializeField, Range(2, 6)] private int requiredCount = 3;
|
||||
|
||||
[Tooltip("Использовать фиксированное число очков вместо суммы")]
|
||||
[SerializeField] private bool useFixedScore;
|
||||
|
||||
[Tooltip("Фиксированное число очков (если useFixedScore = true)")]
|
||||
[SerializeField] private int fixedScore;
|
||||
|
||||
public override int Calculate(IReadOnlyList<IDie> dice)
|
||||
{
|
||||
int[] values = DiceCheckUtility.ExtractValues(dice);
|
||||
|
||||
if (!DiceCheckUtility.NOfAKind(values, requiredCount))
|
||||
return 0;
|
||||
|
||||
return useFixedScore ? fixedScore : DiceCheckUtility.Sum(values);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static NOfAKindCategorySO CreateForTest(string id, string displayName, int count, bool fixedScoreMode = false, int score = 0)
|
||||
{
|
||||
var so = CreateInstance<NOfAKindCategorySO>();
|
||||
so.SetTestData(id, displayName);
|
||||
so.requiredCount = count;
|
||||
so.useFixedScore = fixedScoreMode;
|
||||
so.fixedScore = score;
|
||||
return so;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using YachtDice.Dice;
|
||||
|
||||
namespace YachtDice.Categories
|
||||
{
|
||||
/// <summary>
|
||||
/// Категория Стрит: последовательность заданной длины.
|
||||
/// При совпадении возвращает фиксированное число очков.
|
||||
/// Малый стрит: runLength=4, fixedScore=30. Большой стрит: runLength=5, fixedScore=40.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "StraightCategory", menuName = "YachtDice/Categories/Straight")]
|
||||
public class StraightCategorySO : CategoryDefinitionSO
|
||||
{
|
||||
[Header("Scoring")]
|
||||
[Tooltip("Требуемая длина последовательности")]
|
||||
[SerializeField, Range(3, 6)] private int runLength = 4;
|
||||
|
||||
[Tooltip("Фиксированное число очков")]
|
||||
[SerializeField] private int fixedScore = 30;
|
||||
|
||||
public override int Calculate(IReadOnlyList<IDie> dice)
|
||||
{
|
||||
int[] values = DiceCheckUtility.ExtractValues(dice);
|
||||
return DiceCheckUtility.HasStraightRun(values, runLength) ? fixedScore : 0;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static StraightCategorySO CreateForTest(string id, string displayName, int run, int score)
|
||||
{
|
||||
var so = CreateInstance<StraightCategorySO>();
|
||||
so.SetTestData(id, displayName);
|
||||
so.runLength = run;
|
||||
so.fixedScore = score;
|
||||
return so;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using YachtDice.Dice;
|
||||
|
||||
namespace YachtDice.Categories
|
||||
{
|
||||
/// <summary>
|
||||
/// Категория «Шанс»: суммирует все дайсы без условий.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "SumAllCategory", menuName = "YachtDice/Categories/Sum All (Chance)")]
|
||||
public class SumAllCategorySO : CategoryDefinitionSO
|
||||
{
|
||||
public override int Calculate(IReadOnlyList<IDie> dice)
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = 0; i < dice.Count; i++)
|
||||
sum += dice[i].Value;
|
||||
return sum;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static SumAllCategorySO CreateForTest(string id, string displayName)
|
||||
{
|
||||
var so = CreateInstance<SumAllCategorySO>();
|
||||
so.SetTestData(id, displayName);
|
||||
return so;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using YachtDice.Dice;
|
||||
|
||||
namespace YachtDice.Categories
|
||||
{
|
||||
/// <summary>
|
||||
/// Категория верхней секции: суммирует все дайсы с заданным значением.
|
||||
/// Используется для Единиц (1), Двоек (2), ... Шестёрок (6).
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "SumOfValueCategory", menuName = "YachtDice/Categories/Sum Of Value")]
|
||||
public class SumOfValueCategorySO : CategoryDefinitionSO
|
||||
{
|
||||
[Header("Scoring")]
|
||||
[Tooltip("Значение грани для суммирования (1-6)")]
|
||||
[SerializeField, Range(1, 6)] private int targetValue = 1;
|
||||
|
||||
public int TargetValue => targetValue;
|
||||
|
||||
public override int Calculate(IReadOnlyList<IDie> dice)
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = 0; i < dice.Count; i++)
|
||||
if (dice[i].Value == targetValue) sum += targetValue;
|
||||
return sum;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static SumOfValueCategorySO CreateForTest(string id, string displayName, int target)
|
||||
{
|
||||
var so = CreateInstance<SumOfValueCategorySO>();
|
||||
so.SetTestData(id, displayName, upperSection: true);
|
||||
so.targetValue = target;
|
||||
return so;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using YachtDice.Dice;
|
||||
|
||||
namespace YachtDice.Categories
|
||||
{
|
||||
/// <summary>
|
||||
/// Статические хелперы для проверки комбинаций дайсов.
|
||||
/// Перенесены из CategoryScorer для переиспользования в конкретных SO-категориях.
|
||||
/// </summary>
|
||||
public static class DiceCheckUtility
|
||||
{
|
||||
/// <summary>Извлекает массив значений из абстрактных дайсов.</summary>
|
||||
public static int[] ExtractValues(IReadOnlyList<IDie> dice)
|
||||
{
|
||||
int[] values = new int[dice.Count];
|
||||
for (int i = 0; i < dice.Count; i++)
|
||||
values[i] = dice[i].Value;
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>Сумма дайсов, показывающих конкретное значение.</summary>
|
||||
public static int SumOfValue(int[] values, int target)
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
if (values[i] == target) sum += target;
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// <summary>Сумма всех дайсов.</summary>
|
||||
public static int Sum(int[] values)
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = 0; i < values.Length; i++) sum += values[i];
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// <summary>Есть ли N или более одинаковых значений.</summary>
|
||||
public static bool NOfAKind(int[] values, int n)
|
||||
{
|
||||
int[] counts = new int[7];
|
||||
for (int i = 0; i < values.Length; i++) counts[values[i]]++;
|
||||
for (int v = 1; v <= 6; v++)
|
||||
if (counts[v] >= n) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Проверяет фулл-хаус (3 + 2 одинаковых).</summary>
|
||||
public static bool IsFullHouse(int[] values)
|
||||
{
|
||||
int[] counts = new int[7];
|
||||
for (int i = 0; i < values.Length; i++) counts[values[i]]++;
|
||||
bool hasTwo = false, hasThree = false;
|
||||
for (int v = 1; v <= 6; v++)
|
||||
{
|
||||
if (counts[v] == 2) hasTwo = true;
|
||||
if (counts[v] == 3) hasThree = true;
|
||||
}
|
||||
return hasTwo && hasThree;
|
||||
}
|
||||
|
||||
/// <summary>Есть ли последовательность заданной длины.</summary>
|
||||
public static bool HasStraightRun(int[] values, int runLength)
|
||||
{
|
||||
bool[] present = new bool[7];
|
||||
for (int i = 0; i < values.Length; i++) present[values[i]] = true;
|
||||
|
||||
int consecutive = 0;
|
||||
for (int v = 1; v <= 6; v++)
|
||||
{
|
||||
consecutive = present[v] ? consecutive + 1 : 0;
|
||||
if (consecutive >= runLength) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user