diff --git a/Assets/Scripts/Game/DebugGameInput.cs b/Assets/Scripts/Game/DebugGameInput.cs new file mode 100644 index 0000000..f1b520f --- /dev/null +++ b/Assets/Scripts/Game/DebugGameInput.cs @@ -0,0 +1,34 @@ +using UnityEngine; + +public sealed class DebugGameInput : MonoBehaviour +{ + [SerializeField] private GameManager gameManager; + + private void Update() + { + if (Input.GetKeyDown(KeyCode.Space)) + gameManager.Roll(); + + // 1-5: toggle lock on dice 0-4 + if (Input.GetKeyDown(KeyCode.Alpha1)) gameManager.ToggleDiceLock(0); + if (Input.GetKeyDown(KeyCode.Alpha2)) gameManager.ToggleDiceLock(1); + if (Input.GetKeyDown(KeyCode.Alpha3)) gameManager.ToggleDiceLock(2); + if (Input.GetKeyDown(KeyCode.Alpha4)) gameManager.ToggleDiceLock(3); + if (Input.GetKeyDown(KeyCode.Alpha5)) gameManager.ToggleDiceLock(4); + + // Score categories + if (Input.GetKeyDown(KeyCode.F1)) gameManager.ScoreInCategory(YachtCategory.Ones); + if (Input.GetKeyDown(KeyCode.F2)) gameManager.ScoreInCategory(YachtCategory.Twos); + if (Input.GetKeyDown(KeyCode.F3)) gameManager.ScoreInCategory(YachtCategory.Threes); + if (Input.GetKeyDown(KeyCode.F4)) gameManager.ScoreInCategory(YachtCategory.Fours); + if (Input.GetKeyDown(KeyCode.F5)) gameManager.ScoreInCategory(YachtCategory.Fives); + if (Input.GetKeyDown(KeyCode.F6)) gameManager.ScoreInCategory(YachtCategory.Sixes); + if (Input.GetKeyDown(KeyCode.F7)) gameManager.ScoreInCategory(YachtCategory.ThreeOfAKind); + if (Input.GetKeyDown(KeyCode.F8)) gameManager.ScoreInCategory(YachtCategory.FourOfAKind); + if (Input.GetKeyDown(KeyCode.F9)) gameManager.ScoreInCategory(YachtCategory.FullHouse); + if (Input.GetKeyDown(KeyCode.F10)) gameManager.ScoreInCategory(YachtCategory.SmallStraight); + if (Input.GetKeyDown(KeyCode.F11)) gameManager.ScoreInCategory(YachtCategory.LargeStraight); + if (Input.GetKeyDown(KeyCode.F12)) gameManager.ScoreInCategory(YachtCategory.Yacht); + if (Input.GetKeyDown(KeyCode.C)) gameManager.ScoreInCategory(YachtCategory.Chance); + } +} diff --git a/Assets/Scripts/Game/DiceManager.cs b/Assets/Scripts/Game/DiceManager.cs new file mode 100644 index 0000000..44b624f --- /dev/null +++ b/Assets/Scripts/Game/DiceManager.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +public sealed class DiceManager : MonoBehaviour +{ + [SerializeField] private List diceRollers = new(); + + public event Action OnAllDiceSettled; + public event Action OnDieSettled; + + public int DiceCount => diceRollers.Count; + + private bool[] locked; + private int[] currentValues; + private int pendingCount; + + private void Awake() + { + int count = diceRollers.Count; + locked = new bool[count]; + currentValues = new int[count]; + } + + public bool IsLocked(int index) => locked[index]; + + public void ToggleLock(int index) + { + locked[index] = !locked[index]; + } + + public void SetLocked(int index, bool isLocked) + { + locked[index] = isLocked; + } + + public void UnlockAll() + { + for (int i = 0; i < locked.Length; i++) locked[i] = false; + } + + public void RollUnlocked() + { + for (int i = 0; i < diceRollers.Count; i++) + if (diceRollers[i].IsRolling) return; + + pendingCount = 0; + + for (int i = 0; i < diceRollers.Count; i++) + { + if (locked[i]) continue; + + pendingCount++; + int capturedIndex = i; + + void Handler(int value) + { + diceRollers[capturedIndex].OnRollFinished -= Handler; + currentValues[capturedIndex] = value; + OnDieSettled?.Invoke(capturedIndex, value); + + pendingCount--; + if (pendingCount <= 0) + OnAllDiceSettled?.Invoke(); + } + + diceRollers[i].OnRollFinished += Handler; + diceRollers[i].Roll(); + } + + if (pendingCount == 0) + OnAllDiceSettled?.Invoke(); + } + + public int[] GetCurrentValues() + { + int[] copy = new int[currentValues.Length]; + Array.Copy(currentValues, copy, currentValues.Length); + return copy; + } + + public int GetValue(int index) => currentValues[index]; + + public bool IsAnyRolling + { + get + { + for (int i = 0; i < diceRollers.Count; i++) + if (diceRollers[i].IsRolling) return true; + return false; + } + } + + public void ReadAllValues() + { + for (int i = 0; i < diceRollers.Count; i++) + { + var diceComponent = diceRollers[i].GetComponent(); + if (diceComponent != null && diceComponent.TryGetTopValue(out int val)) + currentValues[i] = val; + } + } +} diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs new file mode 100644 index 0000000..55840fa --- /dev/null +++ b/Assets/Scripts/Game/GameManager.cs @@ -0,0 +1,100 @@ +using System; +using UnityEngine; + +public sealed class GameManager : MonoBehaviour +{ + [Header("References")] + [SerializeField] private DiceManager diceManager; + [SerializeField] private ScoringSystem scoringSystem; + + [Header("Settings")] + [SerializeField] private int maxRollsPerTurn = 3; + + public int CurrentRoll { get; private set; } + public int CurrentTurn { get; private set; } + + public bool CanRoll => CurrentRoll < maxRollsPerTurn && !diceManager.IsAnyRolling; + public bool CanScore => CurrentRoll > 0 && !diceManager.IsAnyRolling; + public bool IsGameOver => scoringSystem.IsComplete; + + public event Action OnTurnStarted; + public event Action OnRollComplete; + public event Action OnScored; + public event Action OnGameOver; + + private void Start() + { + StartNewGame(); + } + + public void StartNewGame() + { + scoringSystem.ResetScorecard(); + CurrentTurn = 0; + StartNewTurn(); + } + + private void StartNewTurn() + { + CurrentTurn++; + CurrentRoll = 0; + diceManager.UnlockAll(); + OnTurnStarted?.Invoke(CurrentTurn); + Debug.Log($"=== Turn {CurrentTurn} ==="); + } + + public void Roll() + { + if (!CanRoll) return; + + CurrentRoll++; + diceManager.OnAllDiceSettled += HandleAllDiceSettled; + diceManager.RollUnlocked(); + } + + private void HandleAllDiceSettled() + { + diceManager.OnAllDiceSettled -= HandleAllDiceSettled; + + int[] values = diceManager.GetCurrentValues(); + Debug.Log($"Roll {CurrentRoll}/{maxRollsPerTurn} | Dice: [{string.Join(", ", values)}]"); + + OnRollComplete?.Invoke(CurrentRoll); + } + + public void ToggleDiceLock(int index) + { + if (diceManager.IsAnyRolling) return; + if (CurrentRoll == 0) return; + diceManager.ToggleLock(index); + + bool isLocked = diceManager.IsLocked(index); + Debug.Log($"Die {index + 1} (value={diceManager.GetValue(index)}): {(isLocked ? "LOCKED" : "UNLOCKED")}"); + } + + public void ScoreInCategory(YachtCategory category) + { + if (!CanScore) return; + if (scoringSystem.IsCategoryUsed(category)) return; + + int[] values = diceManager.GetCurrentValues(); + ScoreResult result = scoringSystem.ScoreCategory(values, category); + + Debug.Log($"Scored {category}: base={result.BaseScore}, " + + $"bonus=+{result.FlatBonus}, mult=x{result.Multiplier:F1}, " + + $"FINAL={result.FinalScore} | Total={scoringSystem.TotalScore}"); + + OnScored?.Invoke(category, result.FinalScore); + + if (scoringSystem.IsComplete) + { + int total = scoringSystem.TotalScore; + Debug.Log($"*** GAME OVER *** Total Score: {total}"); + OnGameOver?.Invoke(total); + } + else + { + StartNewTurn(); + } + } +} diff --git a/Assets/Scripts/Modifiers/BonusForOnes.cs b/Assets/Scripts/Modifiers/BonusForOnes.cs new file mode 100644 index 0000000..2db89aa --- /dev/null +++ b/Assets/Scripts/Modifiers/BonusForOnes.cs @@ -0,0 +1,18 @@ +using UnityEngine; + +[CreateAssetMenu(fileName = "BonusForOnes", menuName = "YachtDice/Modifiers/Bonus For Ones")] +public sealed class BonusForOnes : ScoreModifier +{ + [SerializeField] private int bonusPerOne = 10; + + public override ModifierPhase Phase => ModifierPhase.Additive; + + public override void Apply(ref ScoreResult result) + { + int count = 0; + for (int i = 0; i < result.DiceValues.Length; i++) + if (result.DiceValues[i] == 1) count++; + + result.FlatBonus += bonusPerOne * count; + } +} diff --git a/Assets/Scripts/Modifiers/MultiplierForSixes.cs b/Assets/Scripts/Modifiers/MultiplierForSixes.cs new file mode 100644 index 0000000..4fc33cf --- /dev/null +++ b/Assets/Scripts/Modifiers/MultiplierForSixes.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +[CreateAssetMenu(fileName = "MultiplierForSixes", menuName = "YachtDice/Modifiers/Multiplier For Sixes")] +public sealed class MultiplierForSixes : ScoreModifier +{ + [SerializeField] private float multiplierPerSix = 6f; + + public override ModifierPhase Phase => ModifierPhase.Multiplicative; + + public override void Apply(ref ScoreResult result) + { + for (int i = 0; i < result.DiceValues.Length; i++) + if (result.DiceValues[i] == 6) + result.Multiplier *= multiplierPerSix; + } +} diff --git a/Assets/Scripts/Modifiers/ScoreModifier.cs b/Assets/Scripts/Modifiers/ScoreModifier.cs new file mode 100644 index 0000000..615cc19 --- /dev/null +++ b/Assets/Scripts/Modifiers/ScoreModifier.cs @@ -0,0 +1,19 @@ +using UnityEngine; + +public abstract class ScoreModifier : ScriptableObject +{ + public enum ModifierPhase + { + Additive, + Multiplicative + } + + [SerializeField] private string displayName; + [SerializeField] [TextArea] private string description; + + public string DisplayName => displayName; + public string Description => description; + + public abstract ModifierPhase Phase { get; } + public abstract void Apply(ref ScoreResult result); +} diff --git a/Assets/Scripts/Scoring/CategoryScorer.cs b/Assets/Scripts/Scoring/CategoryScorer.cs new file mode 100644 index 0000000..3be8ca2 --- /dev/null +++ b/Assets/Scripts/Scoring/CategoryScorer.cs @@ -0,0 +1,79 @@ +using System; + +public static class CategoryScorer +{ + public static int Calculate(int[] dice, YachtCategory category) + { + if (dice == null || dice.Length != 5) + throw new ArgumentException("Exactly 5 dice values required."); + + return category switch + { + YachtCategory.Ones => SumOfValue(dice, 1), + YachtCategory.Twos => SumOfValue(dice, 2), + YachtCategory.Threes => SumOfValue(dice, 3), + YachtCategory.Fours => SumOfValue(dice, 4), + YachtCategory.Fives => SumOfValue(dice, 5), + YachtCategory.Sixes => SumOfValue(dice, 6), + YachtCategory.ThreeOfAKind => NOfAKind(dice, 3) ? Sum(dice) : 0, + YachtCategory.FourOfAKind => NOfAKind(dice, 4) ? Sum(dice) : 0, + YachtCategory.FullHouse => IsFullHouse(dice) ? 25 : 0, + YachtCategory.SmallStraight => HasStraightRun(dice, 4) ? 30 : 0, + YachtCategory.LargeStraight => HasStraightRun(dice, 5) ? 40 : 0, + YachtCategory.Yacht => NOfAKind(dice, 5) ? 50 : 0, + YachtCategory.Chance => Sum(dice), + _ => 0 + }; + } + + private static int SumOfValue(int[] dice, int target) + { + int sum = 0; + for (int i = 0; i < dice.Length; i++) + if (dice[i] == target) sum += target; + return sum; + } + + private static int Sum(int[] dice) + { + int sum = 0; + for (int i = 0; i < dice.Length; i++) sum += dice[i]; + return sum; + } + + private static bool NOfAKind(int[] dice, int n) + { + int[] counts = new int[7]; + for (int i = 0; i < dice.Length; i++) counts[dice[i]]++; + for (int v = 1; v <= 6; v++) + if (counts[v] >= n) return true; + return false; + } + + private static bool IsFullHouse(int[] dice) + { + int[] counts = new int[7]; + for (int i = 0; i < dice.Length; i++) counts[dice[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; + } + + private static bool HasStraightRun(int[] dice, int runLength) + { + bool[] present = new bool[7]; + for (int i = 0; i < dice.Length; i++) present[dice[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; + } +} diff --git a/Assets/Scripts/Scoring/ScoreResult.cs b/Assets/Scripts/Scoring/ScoreResult.cs new file mode 100644 index 0000000..f851d00 --- /dev/null +++ b/Assets/Scripts/Scoring/ScoreResult.cs @@ -0,0 +1,26 @@ +using System; +using UnityEngine; + +[Serializable] +public struct ScoreResult +{ + public int BaseScore; + public int FlatBonus; + public float Multiplier; + public int[] DiceValues; + public YachtCategory Category; + + public int FinalScore => Mathf.FloorToInt((BaseScore + FlatBonus) * Multiplier); + + public static ScoreResult Create(int baseScore, int[] diceValues, YachtCategory category) + { + return new ScoreResult + { + BaseScore = baseScore, + FlatBonus = 0, + Multiplier = 1f, + DiceValues = diceValues, + Category = category + }; + } +} diff --git a/Assets/Scripts/Scoring/ScoringSystem.cs b/Assets/Scripts/Scoring/ScoringSystem.cs new file mode 100644 index 0000000..97fb5d1 --- /dev/null +++ b/Assets/Scripts/Scoring/ScoringSystem.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +public sealed class ScoringSystem : MonoBehaviour +{ + [Header("Modifiers")] + [SerializeField] private List activeModifiers = new(); + + public event Action OnCategoryScored; + public event Action OnAllCategoriesScored; + + private readonly Dictionary scorecard = new(); + private readonly HashSet usedCategories = new(); + + public bool IsCategoryUsed(YachtCategory category) => usedCategories.Contains(category); + + public int GetCategoryScore(YachtCategory category) + { + return scorecard.TryGetValue(category, out int score) ? score : -1; + } + + public int TotalScore + { + get + { + int total = 0; + foreach (var kvp in scorecard) total += kvp.Value; + return total; + } + } + + public int CategoriesFilledCount => usedCategories.Count; + + public int TotalCategoryCount => Enum.GetValues(typeof(YachtCategory)).Length; + + public bool IsComplete => CategoriesFilledCount >= TotalCategoryCount; + + public void AddModifier(ScoreModifier modifier) + { + if (modifier != null && !activeModifiers.Contains(modifier)) + activeModifiers.Add(modifier); + } + + public void RemoveModifier(ScoreModifier modifier) + { + activeModifiers.Remove(modifier); + } + + public IReadOnlyList ActiveModifiers => activeModifiers; + + public ScoreResult PreviewScore(int[] diceValues, YachtCategory category) + { + int baseScore = CategoryScorer.Calculate(diceValues, category); + ScoreResult result = ScoreResult.Create(baseScore, diceValues, category); + ApplyModifiers(ref result); + return result; + } + + public ScoreResult ScoreCategory(int[] diceValues, YachtCategory category) + { + if (usedCategories.Contains(category)) + throw new InvalidOperationException($"Category {category} has already been scored."); + + ScoreResult result = PreviewScore(diceValues, category); + + int finalScore = result.FinalScore; + scorecard[category] = finalScore; + usedCategories.Add(category); + + OnCategoryScored?.Invoke(category, finalScore); + + if (IsComplete) + OnAllCategoriesScored?.Invoke(TotalScore); + + return result; + } + + private void ApplyModifiers(ref ScoreResult result) + { + // Pass 1: Additive + for (int i = 0; i < activeModifiers.Count; i++) + { + if (activeModifiers[i] == null) continue; + if (activeModifiers[i].Phase == ScoreModifier.ModifierPhase.Additive) + activeModifiers[i].Apply(ref result); + } + + // Pass 2: Multiplicative + for (int i = 0; i < activeModifiers.Count; i++) + { + if (activeModifiers[i] == null) continue; + if (activeModifiers[i].Phase == ScoreModifier.ModifierPhase.Multiplicative) + activeModifiers[i].Apply(ref result); + } + } + + public void ResetScorecard() + { + scorecard.Clear(); + usedCategories.Clear(); + } +} diff --git a/Assets/Scripts/Scoring/YachtCategory.cs b/Assets/Scripts/Scoring/YachtCategory.cs new file mode 100644 index 0000000..e9a7620 --- /dev/null +++ b/Assets/Scripts/Scoring/YachtCategory.cs @@ -0,0 +1,19 @@ +public enum YachtCategory +{ + // Upper Section + Ones, + Twos, + Threes, + Fours, + Fives, + Sixes, + + // Lower Section + ThreeOfAKind, + FourOfAKind, + FullHouse, + SmallStraight, + LargeStraight, + Yacht, + Chance +}