[Add] GameLoop base
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using YachtDice.Categories;
|
||||
using YachtDice.Dice;
|
||||
using YachtDice.Economy;
|
||||
using YachtDice.Scoring;
|
||||
|
||||
namespace YachtDice.Run
|
||||
{
|
||||
public sealed class RunLoopService
|
||||
{
|
||||
private readonly RunBalanceConfigSO _config;
|
||||
private readonly ScoringSystem _scoringSystem;
|
||||
private readonly CurrencyBank _currencyBank;
|
||||
private readonly StoredRollBank _storedRollBank;
|
||||
private readonly RunState _state = new();
|
||||
|
||||
public RunLoopService(
|
||||
RunBalanceConfigSO config,
|
||||
ScoringSystem scoringSystem,
|
||||
CurrencyBank currencyBank,
|
||||
StoredRollBank storedRollBank)
|
||||
{
|
||||
_config = config != null ? config : RunBalanceConfigSO.CreateDefault();
|
||||
_scoringSystem = scoringSystem;
|
||||
_currencyBank = currencyBank;
|
||||
_storedRollBank = storedRollBank;
|
||||
|
||||
if (_storedRollBank != null)
|
||||
_storedRollBank.OnChanged += HandleStoredRollsChanged;
|
||||
|
||||
if (_currencyBank != null)
|
||||
_currencyBank.OnBalanceChanged += HandleCurrencyChanged;
|
||||
}
|
||||
|
||||
public RunState State => _state;
|
||||
public int CurrentCurrency => _currencyBank != null ? _currencyBank.Balance : 0;
|
||||
|
||||
public event Action<RunState> OnRunStarted;
|
||||
public event Action<RunState> OnBetStarted;
|
||||
public event Action<RunState> OnShopOpened;
|
||||
public event Action<RunStageState> OnStageStarted;
|
||||
public event Action<RunStageState, CategoryDefinition, ScoreResult> OnStageCleared;
|
||||
public event Action<RunStageState> OnStageFailed;
|
||||
public event Action<int> OnStoredRollsChanged;
|
||||
public event Action<int> OnCurrencyChanged;
|
||||
public event Action<int, int> OnCycleCompleted;
|
||||
public event Action<int> OnQuotaChanged;
|
||||
public event Action<RunState> OnRunFailed;
|
||||
public event Action<RunPhase> OnPhaseChanged;
|
||||
public event Action OnCategoriesRefreshed;
|
||||
|
||||
public void StartNewRun()
|
||||
{
|
||||
_scoringSystem.ResetScorecard();
|
||||
_storedRollBank.Reset();
|
||||
|
||||
_state.BaseQuota = _config.StartingBaseQuota;
|
||||
_state.BetIndex = 0;
|
||||
_state.StageIndex = 0;
|
||||
_state.CurrentRoll = 0;
|
||||
_state.CurrentStageRollBudget = _config.BaseRollsPerStage;
|
||||
_state.CurrentStageTarget = _config.GetStageTarget(_state.BaseQuota, 0);
|
||||
_state.StoredRolls = _storedRollBank.Value;
|
||||
_state.IsActive = true;
|
||||
_state.IsFailed = false;
|
||||
SetPhase(RunPhase.None);
|
||||
|
||||
OnRunStarted?.Invoke(_state);
|
||||
StartNextBet();
|
||||
}
|
||||
|
||||
public void CompleteShop()
|
||||
{
|
||||
if (_state.Phase != RunPhase.Shop || !_state.IsActive)
|
||||
return;
|
||||
|
||||
StartStage(0);
|
||||
}
|
||||
|
||||
public bool CanRoll()
|
||||
{
|
||||
if (!_state.IsActive || _state.IsFailed)
|
||||
return false;
|
||||
|
||||
return _state.Phase == RunPhase.StageStart
|
||||
|| _state.Phase == RunPhase.CategorySelection
|
||||
|| _state.Phase == RunPhase.Rolling;
|
||||
}
|
||||
|
||||
public bool TryBeginRoll()
|
||||
{
|
||||
if (!CanRoll())
|
||||
return false;
|
||||
|
||||
if (_state.CurrentRoll >= _state.CurrentStageRollBudget)
|
||||
{
|
||||
if (!_storedRollBank.TrySpend(1))
|
||||
return false;
|
||||
|
||||
_state.CurrentStageRollBudget += 1;
|
||||
_state.StoredRolls = _storedRollBank.Value;
|
||||
}
|
||||
|
||||
_state.CurrentRoll += 1;
|
||||
SetPhase(RunPhase.Rolling);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void NotifyRollResolved(IReadOnlyList<IDice> dice)
|
||||
{
|
||||
if (!_state.IsActive || _state.IsFailed || _state.Phase != RunPhase.Rolling)
|
||||
return;
|
||||
|
||||
SetPhase(RunPhase.CategorySelection);
|
||||
|
||||
if (!HasRemainingRollCapacity() && !HasAnyScorableCategory(dice))
|
||||
FailCurrentStage();
|
||||
}
|
||||
|
||||
public bool CanScoreCategory(IReadOnlyList<IDice> dice, CategoryDefinition category)
|
||||
{
|
||||
if (!_state.IsActive || _state.IsFailed || _state.Phase != RunPhase.CategorySelection)
|
||||
return false;
|
||||
|
||||
if (category == null || _scoringSystem.IsCategoryUsed(category))
|
||||
return false;
|
||||
|
||||
if (_state.CurrentRoll <= 0)
|
||||
return false;
|
||||
|
||||
var preview = PreviewScore(dice, category);
|
||||
return preview.FinalScore >= _state.CurrentStageTarget;
|
||||
}
|
||||
|
||||
public ScoreResult PreviewScore(IReadOnlyList<IDice> dice, CategoryDefinition category)
|
||||
{
|
||||
return _scoringSystem.PreviewScore(
|
||||
dice,
|
||||
category,
|
||||
_state.CurrentRoll,
|
||||
GetProgressOrdinal(),
|
||||
CurrentCurrency);
|
||||
}
|
||||
|
||||
public bool HasAnyScorableCategory(IReadOnlyList<IDice> dice)
|
||||
{
|
||||
var catalog = _scoringSystem.Catalog;
|
||||
if (catalog == null)
|
||||
return false;
|
||||
|
||||
var categories = catalog.All;
|
||||
for (var i = 0; i < categories.Count; i++)
|
||||
{
|
||||
if (CanScoreCategory(dice, categories[i]))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasRemainingRollCapacity()
|
||||
{
|
||||
return _state.CurrentRoll < _state.CurrentStageRollBudget || _storedRollBank.Value > 0;
|
||||
}
|
||||
|
||||
public async UniTask<bool> TryScoreCategoryAsync(IReadOnlyList<IDice> dice, CategoryDefinition category)
|
||||
{
|
||||
if (!CanScoreCategory(dice, category))
|
||||
return false;
|
||||
|
||||
var clearedStage = GetCurrentStageState();
|
||||
var result = await _scoringSystem.ScoreCategoryAsync(
|
||||
dice,
|
||||
category,
|
||||
_state.CurrentRoll,
|
||||
GetProgressOrdinal(),
|
||||
CurrentCurrency);
|
||||
|
||||
var remainingRolls = Math.Max(0, _state.CurrentStageRollBudget - _state.CurrentRoll);
|
||||
if (remainingRolls > 0)
|
||||
_storedRollBank.Add(remainingRolls);
|
||||
|
||||
if (_config.StageClearReward > 0 && _currencyBank != null)
|
||||
_currencyBank.Add(_config.StageClearReward);
|
||||
|
||||
SetPhase(RunPhase.StageResolved);
|
||||
OnStageCleared?.Invoke(clearedStage, category, result);
|
||||
|
||||
if (_state.StageIndex >= _config.StageCount - 1)
|
||||
{
|
||||
ResolveBet();
|
||||
}
|
||||
else
|
||||
{
|
||||
StartStage(_state.StageIndex + 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FailCurrentStage()
|
||||
{
|
||||
if (!_state.IsActive || _state.IsFailed)
|
||||
return;
|
||||
|
||||
var failedStage = GetCurrentStageState();
|
||||
_state.IsActive = false;
|
||||
_state.IsFailed = true;
|
||||
SetPhase(RunPhase.RunFailed);
|
||||
|
||||
OnStageFailed?.Invoke(failedStage);
|
||||
OnRunFailed?.Invoke(_state);
|
||||
}
|
||||
|
||||
private void StartNextBet()
|
||||
{
|
||||
_state.BetIndex += 1;
|
||||
_state.StageIndex = 0;
|
||||
_state.CurrentRoll = 0;
|
||||
_state.CurrentStageRollBudget = _config.BaseRollsPerStage;
|
||||
_state.CurrentStageTarget = _config.GetStageTarget(_state.BaseQuota, 0);
|
||||
|
||||
SetPhase(RunPhase.StartBet);
|
||||
OnCategoriesRefreshed?.Invoke();
|
||||
OnBetStarted?.Invoke(_state);
|
||||
|
||||
SetPhase(RunPhase.Shop);
|
||||
OnShopOpened?.Invoke(_state);
|
||||
}
|
||||
|
||||
private void StartStage(int stageIndex)
|
||||
{
|
||||
_state.StageIndex = stageIndex;
|
||||
_state.CurrentRoll = 0;
|
||||
_state.CurrentStageRollBudget = _config.BaseRollsPerStage;
|
||||
_state.CurrentStageTarget = _config.GetStageTarget(_state.BaseQuota, stageIndex);
|
||||
|
||||
SetPhase(RunPhase.StageStart);
|
||||
OnStageStarted?.Invoke(GetCurrentStageState());
|
||||
}
|
||||
|
||||
private void ResolveBet()
|
||||
{
|
||||
SetPhase(RunPhase.BetResolved);
|
||||
|
||||
if (_scoringSystem.IsComplete)
|
||||
{
|
||||
var bonus = _storedRollBank.Value * _config.CycleStoredRollBonusMultiplier;
|
||||
if (bonus > 0 && _currencyBank != null)
|
||||
_currencyBank.Add(bonus);
|
||||
|
||||
OnCycleCompleted?.Invoke(bonus, _storedRollBank.Value);
|
||||
|
||||
_scoringSystem.ResetScorecard();
|
||||
var nextQuota = _config.GetNextQuota(_state.BaseQuota);
|
||||
_state.BaseQuota = nextQuota;
|
||||
OnQuotaChanged?.Invoke(nextQuota);
|
||||
SetPhase(RunPhase.CycleResolved);
|
||||
}
|
||||
|
||||
StartNextBet();
|
||||
}
|
||||
|
||||
private RunStageState GetCurrentStageState()
|
||||
{
|
||||
return new RunStageState(_state.StageIndex, (RunStageType)Math.Min(_state.StageIndex, 2), _state.CurrentStageTarget);
|
||||
}
|
||||
|
||||
private int GetProgressOrdinal()
|
||||
{
|
||||
return ((_state.BetIndex - 1) * Math.Max(1, _config.StageCount)) + _state.StageIndex + 1;
|
||||
}
|
||||
|
||||
private void SetPhase(RunPhase phase)
|
||||
{
|
||||
_state.Phase = phase;
|
||||
OnPhaseChanged?.Invoke(phase);
|
||||
}
|
||||
|
||||
private void HandleStoredRollsChanged(int value)
|
||||
{
|
||||
_state.StoredRolls = value;
|
||||
OnStoredRollsChanged?.Invoke(value);
|
||||
}
|
||||
|
||||
private void HandleCurrencyChanged(int value)
|
||||
{
|
||||
OnCurrencyChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user