call gamelogic
This commit is contained in:
parent
c33a6f9d91
commit
f297317b71
|
|
@ -196,6 +196,38 @@ public record ThrowResult
|
|||
/// Trigger events that should fire (e.g., for penalties).
|
||||
/// </summary>
|
||||
public IReadOnlyList<TriggerEvent> Triggers { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Optional overrides for standard lane behavior.
|
||||
/// If null, standard behavior applies (pins reset based on ThrowMode after round).
|
||||
/// </summary>
|
||||
public GameLogicOverrides? Overrides { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional overrides for standard lane behavior.
|
||||
/// Game logic can use these to control next player or pin pattern.
|
||||
/// </summary>
|
||||
public record GameLogicOverrides
|
||||
{
|
||||
/// <summary>
|
||||
/// Override the next player. If set, this player will throw next instead of
|
||||
/// following the standard rotation.
|
||||
/// </summary>
|
||||
public Guid? NextPlayerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Override the pin pattern for the next throw. If set, these pin statuses
|
||||
/// will be used instead of the standard reset behavior.
|
||||
/// Array of 9 PinStatus values (index 0 = pin 1, etc.).
|
||||
/// </summary>
|
||||
public PinStatus[]? PinPattern { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, skip the standard pin reset after round completion.
|
||||
/// Use this when PinPattern provides a custom configuration.
|
||||
/// </summary>
|
||||
public bool SkipStandardPinReset { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
@using Koogle.Web.Store.GameState
|
||||
@using Koogle.Web.Components.Game
|
||||
@inherits FluxorComponent
|
||||
@implements IDisposable
|
||||
|
||||
@inject IState<GameState> GameState
|
||||
@inject IDispatcher Dispatcher
|
||||
|
|
@ -163,9 +164,37 @@
|
|||
|
||||
private int? _selectedNumber;
|
||||
private bool _hasModifiedPins;
|
||||
private ThrowPanelState? _beforeThrowState;
|
||||
|
||||
private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
GameState.StateChanged += OnGameStateChanged;
|
||||
// Store initial state for before/after comparison
|
||||
CaptureBeforeThrowState();
|
||||
}
|
||||
|
||||
private void OnGameStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// After throw is processed and pins are reset, capture new before state
|
||||
if (!_hasModifiedPins)
|
||||
{
|
||||
CaptureBeforeThrowState();
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureBeforeThrowState()
|
||||
{
|
||||
_beforeThrowState = GameState.Value.ThrowPanel;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GameState.StateChanged -= OnGameStateChanged;
|
||||
}
|
||||
|
||||
// Throw can always be confirmed - no pins marked = "Kein Holz" (0 fallen)
|
||||
private bool CanConfirmThrow => true;
|
||||
|
||||
|
|
@ -229,59 +258,31 @@
|
|||
|
||||
private async Task ConfirmThrow(bool isGutter, bool isLeftGutter)
|
||||
{
|
||||
var fallenPins = GameState.Value.ThrowPanel.CountFallenPins();
|
||||
var bellValue = GameState.Value.ThrowPanel.BellValue;
|
||||
var currentThrowPanel = GameState.Value.ThrowPanel;
|
||||
var fallenPins = currentThrowPanel.CountFallenPins();
|
||||
var bellValue = currentThrowPanel.BellValue;
|
||||
|
||||
// Create throw result
|
||||
// Create throw result for UI callback
|
||||
var result = new ThrowResult
|
||||
{
|
||||
FallenPins = fallenPins,
|
||||
IsGutter = isGutter,
|
||||
IsLeftGutter = isLeftGutter,
|
||||
BellValue = bellValue,
|
||||
PinStates = GameState.Value.ThrowPanel.GetPins()
|
||||
PinStates = currentThrowPanel.GetPins()
|
||||
};
|
||||
|
||||
// Calculate new throw panel state
|
||||
var newThrowPanel = GameState.Value.ThrowPanel with
|
||||
{
|
||||
ThrowCounterPerRound = GameState.Value.ThrowPanel.ThrowCounterPerRound + 1,
|
||||
TotalThrowCounter = GameState.Value.ThrowPanel.TotalThrowCounter + 1,
|
||||
BellValue = false // Reset bell after throw
|
||||
};
|
||||
// Get before state (captured at start of input session)
|
||||
var beforeState = _beforeThrowState ?? currentThrowPanel;
|
||||
|
||||
// Check if round is complete
|
||||
if (newThrowPanel.ThrowCounterPerRound >= newThrowPanel.ThrowsPerRound)
|
||||
{
|
||||
newThrowPanel = newThrowPanel with { ThrowCounterPerRound = 0 };
|
||||
// Reset all pins at end of round (both modes)
|
||||
newThrowPanel = newThrowPanel.ResetPins();
|
||||
}
|
||||
else if (newThrowPanel.ThrowMode == ThrowMode.Reposition)
|
||||
{
|
||||
// In Reposition mode, always reset pins after each throw
|
||||
newThrowPanel = newThrowPanel.ResetPins();
|
||||
}
|
||||
else if (newThrowPanel.ThrowMode == ThrowMode.Decrease)
|
||||
{
|
||||
// In Decrease mode: Mark fallen pins as disabled (not available for next throw)
|
||||
newThrowPanel = newThrowPanel.MarkFallenAsDisabled();
|
||||
|
||||
// If all pins are down, reset all pins for a fresh set
|
||||
if (newThrowPanel.AllPinsDown())
|
||||
{
|
||||
newThrowPanel = newThrowPanel.ResetPins();
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch record throw action
|
||||
Dispatcher.Dispatch(new RecordThrowAction(newThrowPanel, GameState.Value.GameModel));
|
||||
// Dispatch record throw action - game logic will handle pin reset and player rotation
|
||||
Dispatcher.Dispatch(new RecordThrowAction(beforeState, currentThrowPanel));
|
||||
|
||||
// Reset local state
|
||||
_selectedNumber = null;
|
||||
_hasModifiedPins = false;
|
||||
|
||||
// Notify parent
|
||||
// Notify parent (for timer/tab switching)
|
||||
await OnThrowCompleted.InvokeAsync(result);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,28 @@ public record LoadCompletedGamesFailureAction(string Error);
|
|||
/// <summary>
|
||||
/// Action to record a throw with pin results.
|
||||
/// </summary>
|
||||
public record RecordThrowAction(ThrowPanelState NewThrowPanelState, object? UpdatedGameModel);
|
||||
/// <param name="BeforeThrowState">State before the throw was made.</param>
|
||||
/// <param name="AfterThrowState">State after the throw was made.</param>
|
||||
public record RecordThrowAction(ThrowPanelState BeforeThrowState, ThrowPanelState AfterThrowState);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched after ProcessThrow completes with game logic result.
|
||||
/// </summary>
|
||||
/// <param name="NewThrowPanelState">Updated throw panel state.</param>
|
||||
/// <param name="UpdatedGameModel">Updated game model from ProcessThrow.</param>
|
||||
/// <param name="ShouldRotatePlayer">Whether to rotate to next player.</param>
|
||||
/// <param name="NextPlayerId">Override: specific player to set as current.</param>
|
||||
/// <param name="PinPattern">Override: custom pin pattern for next throw.</param>
|
||||
/// <param name="IsGameOver">Whether the game has ended.</param>
|
||||
/// <param name="WinnerId">Winner if game is over.</param>
|
||||
public record ProcessThrowResultAction(
|
||||
ThrowPanelState NewThrowPanelState,
|
||||
object? UpdatedGameModel,
|
||||
bool ShouldRotatePlayer,
|
||||
Guid? NextPlayerId,
|
||||
PinStatus[]? PinPattern,
|
||||
bool IsGameOver,
|
||||
Guid? WinnerId);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when throw is recorded successfully.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public class GameEffects
|
|||
private readonly IState<GameState> _gameState;
|
||||
private readonly IGamePersistenceService _persistenceService;
|
||||
private readonly ICurrentClubContext _clubContext;
|
||||
private readonly GameDefinitionRegistry _gameRegistry;
|
||||
|
||||
// Debounce timer for save operations
|
||||
private Timer? _saveDebounceTimer;
|
||||
|
|
@ -29,12 +30,14 @@ public class GameEffects
|
|||
ILogger<GameEffects> logger,
|
||||
IState<GameState> gameState,
|
||||
IGamePersistenceService persistenceService,
|
||||
ICurrentClubContext clubContext)
|
||||
ICurrentClubContext clubContext,
|
||||
GameDefinitionRegistry gameRegistry)
|
||||
{
|
||||
_logger = logger;
|
||||
_gameState = gameState;
|
||||
_persistenceService = persistenceService;
|
||||
_clubContext = clubContext;
|
||||
_gameRegistry = gameRegistry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -246,11 +249,121 @@ public class GameEffects
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles RecordThrowAction - triggers debounced save to database.
|
||||
/// Handles RecordThrowAction - calls ProcessThrow and applies game logic.
|
||||
/// </summary>
|
||||
[EffectMethod]
|
||||
public Task HandleRecordThrow(RecordThrowAction action, IDispatcher dispatcher)
|
||||
{
|
||||
var state = _gameState.Value;
|
||||
var gameTypeName = state.GameTypeName;
|
||||
var currentPlayerId = state.Participants.CurrentPlayerId;
|
||||
|
||||
if (string.IsNullOrEmpty(gameTypeName) || !currentPlayerId.HasValue)
|
||||
{
|
||||
_logger.LogWarning("Cannot process throw: missing game type or player");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Get game logic service (may be null if game type not registered)
|
||||
IGameLogicService? gameLogicService = null;
|
||||
try
|
||||
{
|
||||
gameLogicService = _gameRegistry.GetLogicService(gameTypeName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not resolve game logic service for {GameType}", gameTypeName);
|
||||
}
|
||||
|
||||
// Create AfterThrowState for game logic
|
||||
var beforeSnapshot = CreateThrowPanelSnapshot(action.BeforeThrowState);
|
||||
var afterSnapshot = CreateThrowPanelSnapshot(action.AfterThrowState);
|
||||
var afterThrowState = afterSnapshot.CreateAfterThrowState(beforeSnapshot, currentPlayerId.Value);
|
||||
|
||||
// Calculate new throw panel state with standard lane behavior
|
||||
var newThrowPanel = action.AfterThrowState with
|
||||
{
|
||||
ThrowCounterPerRound = action.AfterThrowState.ThrowCounterPerRound + 1,
|
||||
TotalThrowCounter = action.AfterThrowState.TotalThrowCounter + 1,
|
||||
BellValue = false // Reset bell after throw
|
||||
};
|
||||
|
||||
// Default values
|
||||
bool shouldRotatePlayer = false;
|
||||
Guid? nextPlayerId = null;
|
||||
PinStatus[]? pinPattern = null;
|
||||
bool isGameOver = false;
|
||||
Guid? winnerId = null;
|
||||
object? updatedGameModel = state.GameModel;
|
||||
|
||||
// Call ProcessThrow if game logic service is available
|
||||
if (gameLogicService != null && state.GameModel != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (updatedModel, throwResult) = gameLogicService.ProcessThrow(state.GameModel, afterThrowState);
|
||||
updatedGameModel = updatedModel;
|
||||
shouldRotatePlayer = throwResult.ShouldRotatePlayer;
|
||||
isGameOver = throwResult.IsGameOver;
|
||||
winnerId = throwResult.WinnerId;
|
||||
|
||||
// Check for overrides from game logic
|
||||
if (throwResult.Overrides != null)
|
||||
{
|
||||
nextPlayerId = throwResult.Overrides.NextPlayerId;
|
||||
pinPattern = throwResult.Overrides.PinPattern;
|
||||
|
||||
// If game logic provides pin pattern, use it
|
||||
if (pinPattern != null && pinPattern.Length == 9)
|
||||
{
|
||||
newThrowPanel = ApplyPinPattern(newThrowPanel, pinPattern);
|
||||
}
|
||||
else if (!throwResult.Overrides.SkipStandardPinReset)
|
||||
{
|
||||
// Apply standard pin reset behavior
|
||||
newThrowPanel = ApplyStandardPinReset(newThrowPanel);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No overrides - apply standard pin reset behavior
|
||||
newThrowPanel = ApplyStandardPinReset(newThrowPanel);
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"ProcessThrow completed: pins={Pins}, rotate={Rotate}, gameOver={GameOver}",
|
||||
afterThrowState.PinsKnocked, shouldRotatePlayer, isGameOver);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in ProcessThrow for game type {GameType}", gameTypeName);
|
||||
// Continue with standard behavior on error
|
||||
newThrowPanel = ApplyStandardPinReset(newThrowPanel);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No game logic service - use standard behavior
|
||||
newThrowPanel = ApplyStandardPinReset(newThrowPanel);
|
||||
|
||||
// Standard rotation: rotate after round is complete
|
||||
shouldRotatePlayer = newThrowPanel.ThrowCounterPerRound >= newThrowPanel.ThrowsPerRound;
|
||||
if (shouldRotatePlayer)
|
||||
{
|
||||
newThrowPanel = newThrowPanel with { ThrowCounterPerRound = 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch result action
|
||||
dispatcher.Dispatch(new ProcessThrowResultAction(
|
||||
newThrowPanel,
|
||||
updatedGameModel,
|
||||
shouldRotatePlayer,
|
||||
nextPlayerId,
|
||||
pinPattern,
|
||||
isGameOver,
|
||||
winnerId));
|
||||
|
||||
// Debounce save operations to avoid excessive DB writes
|
||||
_saveDebounceTimer?.Dispose();
|
||||
_saveDebounceTimer = new Timer(
|
||||
|
|
@ -262,6 +375,64 @@ public class GameEffects
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static ThrowPanelSnapshot CreateThrowPanelSnapshot(ThrowPanelState state)
|
||||
{
|
||||
return ThrowPanelSnapshot.FromPins(
|
||||
state.GetPins(),
|
||||
state.ThrowsPerRound,
|
||||
state.ThrowCounterPerRound,
|
||||
state.TotalThrowCounter,
|
||||
state.ThrowMode,
|
||||
state.BellValue);
|
||||
}
|
||||
|
||||
private static ThrowPanelState ApplyPinPattern(ThrowPanelState state, PinStatus[] pattern)
|
||||
{
|
||||
return state with
|
||||
{
|
||||
Pin1 = pattern[0],
|
||||
Pin2 = pattern[1],
|
||||
Pin3 = pattern[2],
|
||||
Pin4 = pattern[3],
|
||||
Pin5 = pattern[4],
|
||||
Pin6 = pattern[5],
|
||||
Pin7 = pattern[6],
|
||||
Pin8 = pattern[7],
|
||||
Pin9 = pattern[8]
|
||||
};
|
||||
}
|
||||
|
||||
private static ThrowPanelState ApplyStandardPinReset(ThrowPanelState state)
|
||||
{
|
||||
// Check if round is complete
|
||||
if (state.ThrowCounterPerRound >= state.ThrowsPerRound)
|
||||
{
|
||||
// Reset throw counter and all pins at end of round
|
||||
var resetState = state with { ThrowCounterPerRound = 0 };
|
||||
return resetState.ResetPins();
|
||||
}
|
||||
|
||||
// Within round - behavior depends on throw mode
|
||||
if (state.ThrowMode == ThrowMode.Reposition)
|
||||
{
|
||||
// Reposition: reset pins after each throw
|
||||
return state.ResetPins();
|
||||
}
|
||||
|
||||
if (state.ThrowMode == ThrowMode.Decrease)
|
||||
{
|
||||
// Decrease: mark fallen as disabled, check for all-down reset
|
||||
var newState = state.MarkFallenAsDisabled();
|
||||
if (newState.AllPinsDown())
|
||||
{
|
||||
return newState.ResetPins();
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles UndoThrowSuccessAction - triggers debounced save after undo.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -181,12 +181,12 @@ public static class GameReducers
|
|||
// Throw Reducers
|
||||
|
||||
/// <summary>
|
||||
/// Handles RecordThrowAction - updates state and adds snapshot to undo stack.
|
||||
/// Handles RecordThrowAction - creates undo snapshot before effect processes throw.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static GameState OnRecordThrow(GameState state, RecordThrowAction action)
|
||||
{
|
||||
// Create snapshot of current state before applying throw
|
||||
// Create snapshot of current state before throw is processed
|
||||
var snapshot = new GameSnapshot
|
||||
{
|
||||
ThrowPanel = state.ThrowPanel,
|
||||
|
|
@ -195,12 +195,45 @@ public static class GameReducers
|
|||
};
|
||||
|
||||
return state with
|
||||
{
|
||||
UndoStack = state.UndoStack.Add(snapshot),
|
||||
IsLoading = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ProcessThrowResultAction - applies game logic result with optional overrides.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static GameState OnProcessThrowResult(GameState state, ProcessThrowResultAction action)
|
||||
{
|
||||
var newState = state with
|
||||
{
|
||||
ThrowPanel = action.NewThrowPanelState,
|
||||
GameModel = action.UpdatedGameModel,
|
||||
UndoStack = state.UndoStack.Add(snapshot),
|
||||
IsLoading = false,
|
||||
IsSaving = true
|
||||
};
|
||||
|
||||
// Apply player rotation or override
|
||||
if (action.NextPlayerId.HasValue)
|
||||
{
|
||||
// Game logic specifies exact next player
|
||||
newState = newState with
|
||||
{
|
||||
Participants = newState.Participants.SetCurrentPlayer(action.NextPlayerId.Value)
|
||||
};
|
||||
}
|
||||
else if (action.ShouldRotatePlayer)
|
||||
{
|
||||
// Standard rotation to next player
|
||||
newState = newState with
|
||||
{
|
||||
Participants = newState.Participants.NextPlayer()
|
||||
};
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue