[Add] Master Category

This commit is contained in:
2026-03-06 23:29:46 +07:00
parent 9580d76a53
commit 73b941d5eb
42 changed files with 817 additions and 21 deletions
@@ -12,9 +12,13 @@ namespace YachtDice.Categories
public class CategoryCatalog : ScriptableObject
{
[SerializeField] private List<CategoryDefinition> categories = new();
[SerializeField] private int upperBonusThreshold = 63;
[SerializeField] private int upperBonusValue = 35;
public IReadOnlyList<CategoryDefinition> All => categories;
public int Count => categories.Count;
public int UpperBonusThreshold => upperBonusThreshold;
public int UpperBonusValue => upperBonusValue;
public CategoryDefinition FindById(string id)
{
@@ -38,10 +42,12 @@ namespace YachtDice.Categories
}
#if UNITY_EDITOR
public static CategoryCatalog CreateForTest(List<CategoryDefinition> defs)
public static CategoryCatalog CreateForTest(List<CategoryDefinition> defs, int upperBonusThreshold = 63, int upperBonusValue = 35)
{
var catalog = CreateInstance<CategoryCatalog>();
catalog.categories = defs ?? new List<CategoryDefinition>();
catalog.upperBonusThreshold = upperBonusThreshold;
catalog.upperBonusValue = upperBonusValue;
return catalog;
}
#endif
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using UnityEngine;
using YachtDice.Dice;
namespace YachtDice.Categories
{
[CreateAssetMenu(fileName = "MasterGroupedSetsCategory", menuName = "YachtDice/Categories/Master/Grouped Sets")]
public class MasterGroupedSetsCategory : CategoryDefinition
{
[Header("Scoring")]
[Tooltip("How many equal dice each group must contain.")]
[SerializeField, Min(1)] private int groupSize = 2;
[Tooltip("How many groups are required.")]
[SerializeField, Min(1)] private int requiredGroups = 3;
[Tooltip("Flat bonus added when the grouped pattern is valid.")]
[SerializeField] private int flatBonus;
public override int Calculate(IReadOnlyList<IDice> dice)
{
var values = DiceCheckUtility.ExtractValues(dice);
if (!DiceCheckUtility.TryGetGroupedSetSum(values, groupSize, requiredGroups, out var groupedSum))
return 0;
return groupedSum + flatBonus;
}
#if UNITY_EDITOR
public static MasterGroupedSetsCategory CreateForTest(
string id,
string displayName,
int groupSize,
int requiredGroups,
int flatBonus = 0)
{
var so = CreateInstance<MasterGroupedSetsCategory>();
so.SetTestData(id, displayName);
so.groupSize = groupSize;
so.requiredGroups = requiredGroups;
so.flatBonus = flatBonus;
return so;
}
#endif
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f1f7c8d8f3a4a34bb5327f9fd0dc003
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using UnityEngine;
using YachtDice.Dice;
namespace YachtDice.Categories
{
[CreateAssetMenu(fileName = "MasterKindCategory", menuName = "YachtDice/Categories/Master/N Of A Kind")]
public class MasterKindCategory : CategoryDefinition
{
[Header("Scoring")]
[Tooltip("How many matching dice are required.")]
[SerializeField, Min(1)] private int requiredCount = 4;
[Tooltip("Additional score equals matched value multiplied by this number.")]
[SerializeField] private int valueBonusMultiplier = 1;
[Tooltip("Extra flat bonus added when the category is valid.")]
[SerializeField] private int flatBonus;
public override int Calculate(IReadOnlyList<IDice> dice)
{
var values = DiceCheckUtility.ExtractValues(dice);
if (!DiceCheckUtility.TryGetBestValueWithAtLeastCount(values, requiredCount, out var matchedValue))
return 0;
return (matchedValue * requiredCount) + (matchedValue * valueBonusMultiplier) + flatBonus;
}
#if UNITY_EDITOR
public static MasterKindCategory CreateForTest(
string id,
string displayName,
int requiredCount,
int valueBonusMultiplier,
int flatBonus = 0)
{
var so = CreateInstance<MasterKindCategory>();
so.SetTestData(id, displayName);
so.requiredCount = requiredCount;
so.valueBonusMultiplier = valueBonusMultiplier;
so.flatBonus = flatBonus;
return so;
}
#endif
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f1f7c8d8f3a4a34bb5327f9fd0dc004
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using UnityEngine;
using YachtDice.Dice;
namespace YachtDice.Categories
{
[CreateAssetMenu(fileName = "MasterStraightCategory", menuName = "YachtDice/Categories/Master/Straight")]
public class MasterStraightCategory : CategoryDefinition
{
[Header("Scoring")]
[Tooltip("Flat bonus added to the dice sum when the straight is valid.")]
[SerializeField] private int flatBonus;
public override int Calculate(IReadOnlyList<IDice> dice)
{
var values = DiceCheckUtility.ExtractValues(dice);
if (!DiceCheckUtility.IsExactStraightOneToSix(values))
return 0;
return DiceCheckUtility.Sum(values) + flatBonus;
}
#if UNITY_EDITOR
public static MasterStraightCategory CreateForTest(string id, string displayName, int flatBonus = 0)
{
var so = CreateInstance<MasterStraightCategory>();
so.SetTestData(id, displayName);
so.flatBonus = flatBonus;
return so;
}
#endif
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f1f7c8d8f3a4a34bb5327f9fd0dc002
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using UnityEngine;
using YachtDice.Dice;
namespace YachtDice.Categories
{
[CreateAssetMenu(fileName = "MasterValueCategory", menuName = "YachtDice/Categories/Master/Value")]
public class MasterValueCategory : CategoryDefinition
{
[field: Header("Scoring")]
[field: Tooltip("Dice face value to collect.")]
[field: SerializeField, Range(1, 6)] public int TargetValue { get; private set; } = 1;
[field: Tooltip("Flat bonus added when at least one matching die is present.")]
[field: SerializeField] public int CategoryBonus { get; private set; }
public override int Calculate(IReadOnlyList<IDice> dice)
{
var values = DiceCheckUtility.ExtractValues(dice);
var sum = DiceCheckUtility.SumOfValue(values, TargetValue);
return sum > 0 ? sum + CategoryBonus : 0;
}
#if UNITY_EDITOR
public static MasterValueCategory CreateForTest(string id, string displayName, int targetValue, int categoryBonus = 0)
{
var so = CreateInstance<MasterValueCategory>();
so.SetTestData(id, displayName, upperSection: true);
so.TargetValue = targetValue;
so.CategoryBonus = categoryBonus;
return so;
}
#endif
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f1f7c8d8f3a4a34bb5327f9fd0dc001
@@ -102,5 +102,83 @@ namespace YachtDice.Categories
return false;
}
public static int[] BuildCounts(int[] values)
{
var counts = new int[7];
if (values == null)
return counts;
for (var i = 0; i < values.Length; i++)
{
var value = values[i];
if (value >= 1 && value <= 6)
counts[value]++;
}
return counts;
}
public static bool TryGetBestValueWithAtLeastCount(int[] values, int requiredCount, out int matchedValue)
{
var counts = BuildCounts(values);
for (var value = 6; value >= 1; value--)
{
if (counts[value] >= requiredCount)
{
matchedValue = value;
return true;
}
}
matchedValue = 0;
return false;
}
public static bool TryGetGroupedSetSum(int[] values, int groupSize, int requiredGroups, out int groupedSum)
{
if (groupSize <= 0 || requiredGroups <= 0)
{
groupedSum = 0;
return false;
}
var counts = BuildCounts(values);
var foundGroups = 0;
groupedSum = 0;
for (var value = 6; value >= 1; value--)
{
if (counts[value] < groupSize)
continue;
foundGroups++;
groupedSum += value * groupSize;
if (foundGroups >= requiredGroups)
return true;
}
groupedSum = 0;
return false;
}
public static bool IsExactStraightOneToSix(int[] values)
{
if (values == null || values.Length != 6)
return false;
var counts = BuildCounts(values);
for (var value = 1; value <= 6; value++)
{
if (counts[value] != 1)
return false;
}
return true;
}
}
}
@@ -0,0 +1,153 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using YachtDice.Categories;
using YachtDice.Dice;
using YachtDice.Scoring;
using YachtDice.UI.Presentation;
namespace YachtDice.Tests
{
public class MasterCategoryTests
{
private readonly List<Object> _createdAssets = new();
private DiceDefinition _standardDice;
[SetUp]
public void SetUp()
{
_standardDice = DiceDefinition.CreateForTest<StandardDice>("d6", "d6");
_createdAssets.Add(_standardDice);
}
[TearDown]
public void TearDown()
{
foreach (var scoring in Object.FindObjectsByType<ScoringSystem>(FindObjectsSortMode.None))
Object.DestroyImmediate(scoring.gameObject);
for (var i = 0; i < _createdAssets.Count; i++)
{
if (_createdAssets[i] != null)
Object.DestroyImmediate(_createdAssets[i]);
}
_createdAssets.Clear();
}
private IReadOnlyList<IDice> CreateDice(params int[] values)
{
var dice = new DiceInstance[values.Length];
for (var i = 0; i < values.Length; i++)
dice[i] = new DiceInstance(_standardDice, values[i]);
return dice;
}
[Test]
public void MasterValueCategory_AddsConfiguredBonus_AndKeepsDefaultZero()
{
var withBonus = MasterValueCategory.CreateForTest("ones_bonus", "Единицы", 1, categoryBonus: 1);
var noBonus = MasterValueCategory.CreateForTest("ones_plain", "Единицы", 1);
_createdAssets.Add(withBonus);
_createdAssets.Add(noBonus);
Assert.AreEqual(5, withBonus.Calculate(CreateDice(1, 1, 1, 2, 3, 4)));
Assert.AreEqual(4, noBonus.Calculate(CreateDice(1, 1, 1, 1, 5, 6)));
Assert.AreEqual(0, withBonus.Calculate(CreateDice(2, 2, 3, 4, 5, 6)));
}
[Test]
public void MasterStraightCategory_RequiresExactOneToSix()
{
var category = MasterStraightCategory.CreateForTest("straight", "Стрит", flatBonus: 20);
_createdAssets.Add(category);
Assert.AreEqual(41, category.Calculate(CreateDice(1, 2, 3, 4, 5, 6)));
Assert.AreEqual(0, category.Calculate(CreateDice(1, 2, 3, 4, 5, 5)));
Assert.AreEqual(0, category.Calculate(CreateDice(1, 2, 3, 4, 5, 6, 6)));
}
[Test]
public void MasterGroupedSetsCategory_SmallFullScoresThreePairs()
{
var category = MasterGroupedSetsCategory.CreateForTest("small_full", "Малый фул", 2, 3, flatBonus: 10);
_createdAssets.Add(category);
Assert.AreEqual(40, category.Calculate(CreateDice(4, 4, 5, 5, 6, 6)));
Assert.AreEqual(28, category.Calculate(CreateDice(1, 1, 3, 3, 5, 5)));
Assert.AreEqual(0, category.Calculate(CreateDice(1, 1, 1, 3, 3, 5)));
}
[Test]
public void MasterGroupedSetsCategory_BigFullScoresTwoTriples()
{
var category = MasterGroupedSetsCategory.CreateForTest("big_full", "Большой фул", 3, 2, flatBonus: 10);
_createdAssets.Add(category);
Assert.AreEqual(43, category.Calculate(CreateDice(5, 5, 5, 6, 6, 6)));
Assert.AreEqual(34, category.Calculate(CreateDice(3, 3, 3, 4, 4, 4)));
Assert.AreEqual(0, category.Calculate(CreateDice(5, 5, 5, 6, 6, 1)));
}
[Test]
public void MasterKindCategory_UsesOnlyRequiredGroup()
{
var kare = MasterKindCategory.CreateForTest("kare", "Каре", 4, valueBonusMultiplier: 1);
var poker = MasterKindCategory.CreateForTest("poker", "Покер", 5, valueBonusMultiplier: 2);
var master = MasterKindCategory.CreateForTest("master", "Мастер", 6, valueBonusMultiplier: 3);
_createdAssets.Add(kare);
_createdAssets.Add(poker);
_createdAssets.Add(master);
Assert.AreEqual(30, kare.Calculate(CreateDice(6, 6, 6, 6, 2, 1)));
Assert.AreEqual(42, poker.Calculate(CreateDice(6, 6, 6, 6, 6, 1)));
Assert.AreEqual(54, master.Calculate(CreateDice(6, 6, 6, 6, 6, 6)));
}
[Test]
public void MasterKindCategory_ScalesToMoreDice_AndPicksBestAvailableGroup()
{
var category = MasterKindCategory.CreateForTest("kare", "Каре", 4, valueBonusMultiplier: 1, flatBonus: 3);
_createdAssets.Add(category);
Assert.AreEqual(33, category.Calculate(CreateDice(6, 6, 6, 6, 6, 2, 1, 1)));
Assert.AreEqual(0, category.Calculate(CreateDice(2, 2, 2, 3, 3, 3, 4, 5)));
}
[Test]
public void ChanceCategory_RemainsUnchanged_WithSixDice()
{
var chance = SumAllCategory.CreateForTest("chance", "Шанс");
_createdAssets.Add(chance);
Assert.AreEqual(21, chance.Calculate(CreateDice(1, 2, 3, 4, 5, 6)));
}
[Test]
public void ScoreSummaryService_UsesCatalogConfiguredUpperBonus()
{
var ones = MasterValueCategory.CreateForTest("ones", "Единицы", 1, categoryBonus: 1);
var twos = MasterValueCategory.CreateForTest("twos", "Двойки", 2, categoryBonus: 2);
var catalog = CategoryCatalog.CreateForTest(new List<CategoryDefinition> { ones, twos }, upperBonusThreshold: 15, upperBonusValue: 50);
_createdAssets.Add(ones);
_createdAssets.Add(twos);
_createdAssets.Add(catalog);
var go = new GameObject("ScoringSystem");
var scoringSystem = go.AddComponent<ScoringSystem>();
scoringSystem.Construct(null, null, catalog, null);
scoringSystem.ScoreCategory(CreateDice(1, 1, 1, 1, 2, 3), ones);
scoringSystem.ScoreCategory(CreateDice(2, 2, 2, 2, 5, 6), twos);
var summaryService = new ScoreSummaryService(scoringSystem, catalog);
var summary = summaryService.Calculate();
Assert.AreEqual(16, summary.UpperSum);
Assert.IsTrue(summary.HasUpperBonus);
Assert.AreEqual(66, summary.DisplayTotal);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f1f7c8d8f3a4a34bb5327f9fd0dc101
@@ -5,9 +5,6 @@ namespace YachtDice.UI.Presentation
{
public sealed class ScoreSummaryService : IScoreSummaryService
{
private const int UpperBonusThreshold = 63;
private const int UpperBonusValue = 35;
private readonly ScoringSystem _scoringSystem;
private readonly CategoryCatalog _categoryCatalog;
@@ -20,11 +17,11 @@ namespace YachtDice.UI.Presentation
public ScoreSummary Calculate()
{
var upperSum = CalculateUpperSum();
var hasUpperBonus = upperSum >= UpperBonusThreshold;
var hasUpperBonus = upperSum >= _categoryCatalog.UpperBonusThreshold;
var total = _scoringSystem.TotalScore;
if (hasUpperBonus)
total += UpperBonusValue;
total += _categoryCatalog.UpperBonusValue;
return new ScoreSummary(total, upperSum, hasUpperBonus);
}
+5 -2
View File
@@ -78,9 +78,12 @@ namespace YachtDice.UI
public void UpdateTotalDisplay(int totalScore, int upperSum, bool hasUpperBonus)
{
var threshold = _catalog != null ? _catalog.UpperBonusThreshold : 63;
var bonusValue = _catalog != null ? _catalog.UpperBonusValue : 35;
totalScoreText.text = totalScore.ToString();
upperSumText.text = $"{upperSum} / 63";
upperBonusText.text = hasUpperBonus ? "+35" : "---";
upperSumText.text = $"{upperSum} / {threshold}";
upperBonusText.text = hasUpperBonus ? $"+{bonusValue}" : "---";
}
public void ResetAll()