/// <summary> /// Creates a state machine for the given rule settings. /// Check https://github.com/Timmeey86/binokel-deluxe/wiki/(Concept)-State-Machines for an overview of possible states and transitions. /// </summary> /// <param name="ruleSettings">The settings for this game.</param> /// <param name="dealerPosition">The Position of the dealing player for this game, starting at zero.</param> public void RefreshStateMachine(RuleSettings ruleSettings, int dealerPosition) { var amountOfCards = ruleSettings.SevensAreIncluded ? 48 : 40; var dabbSize = ruleSettings.SevensAreIncluded && ruleSettings.GameType == GameType.ThreePlayerGame ? 6 : 4; // Configure initial attributes var properties = new SingleGameProperties { NumberOfPlayers = ruleSettings.GameType == GameType.ThreePlayerGame ? 3 : 4, DealerPosition = dealerPosition, RemainingCards = amountOfCards - dabbSize, CurrentPlayerPosition = -1, NextPlayerPosition = -1, TrickWinnerPosition = -1, }; this.stateMachine = new Stateless.StateMachine <SingleGameState, Common.GameTrigger>(SingleGameState.Initial); // Ignore triggers which should not be available. this.stateMachine.OnUnhandledTrigger((state, trigger) => { }); this.stateMachine.Configure(SingleGameState.Initial) .Permit(Common.GameTrigger.GameStarted, SingleGameState.Dealing); this.ConfigureDealingPhase(properties); this.ConfigureBiddingPhase(properties); this.ConfigureDabbPhase(properties); this.ConfigureDurchPhase(); this.ConfigureBettelPhase(); this.ConfigureMeldingPhase(properties); this.ConfigureTrickTakingPhase(properties); this.ConfigureEndPhase(properties); }
private void ConfigureEndPhase(SingleGameProperties properties) { this.stateMachine.Configure(SingleGameState.CountingGameScore) .Permit(Common.GameTrigger.ScoreCalculationFinished, SingleGameState.End) // Let the UI know we are waiting for the final score to be calcualted. .OnEntry(() => this.FireEvent(this.CountingPlayerOrTeamScoresStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "CountingPlayerOrTeamScoresStarted")); this.stateMachine.Configure(SingleGameState.End) .OnEntry(() => this.FireEvent(this.GameFinished, "GameFinished")); }
private void ConfigureDealingPhase(SingleGameProperties properties) { // Dealing phase this.stateMachine.Configure(SingleGameState.Dealing) .Permit(Common.GameTrigger.DealingFinished, SingleGameState.Bidding_WaitingForFirstBid) .OnEntry(() => { properties.CurrentPlayerPosition = (properties.DealerPosition + 1) % properties.NumberOfPlayers; properties.NextPlayerPosition = (properties.DealerPosition + 2) % properties.NumberOfPlayers; this.FireEvent(this.DealingStarted, new PlayerPairEventArgs(properties.CurrentPlayerPosition, properties.NextPlayerPosition), "DealingStarted"); }); }
private void ConfigureMeldingPhase(SingleGameProperties properties) { this.stateMachine.Configure(SingleGameState.Melding) .Permit(Common.GameTrigger.MeldsSeenByAllPlayers, SingleGameState.TrickTaking) // Let the UI know we are waiting to display the melds of all players and wait for confirmation of all // (human) players that they have seen the melds. .OnEntry(() => { this.FireEvent(this.MeldingStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "MeldingStarted"); // Change the current player to the right-hand player of the dealer since this player is always the first to place a card // (except for maybe Bettel and Durch, which is not implemented yet). properties.CurrentPlayerPosition = (properties.DealerPosition + 1) % properties.NumberOfPlayers; }); }
private void ConfigureDabbPhase(SingleGameProperties properties) { this.stateMachine.Configure(SingleGameState.ExchangingCardsWithTheDabb) .Permit(Common.GameTrigger.GoingOut, SingleGameState.CountingGoingOutScore) .Permit(Common.GameTrigger.DurchAnnounced, SingleGameState.Durch) .Permit(Common.GameTrigger.BettelAnnounced, SingleGameState.Bettel) .Permit(Common.GameTrigger.TrumpSelected, SingleGameState.Melding) // Let the UI know we are waiting for the current player to exchange cards with the dabb and do a choice between // going out, selecting a trump or announcing a durch or bettel (if allowed). .OnEntry(() => this.FireEvent(this.ExchangingCardsWithDabbStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "ExchangingCardsWithDabbStarted")); this.stateMachine.Configure(SingleGameState.CountingGoingOutScore) .Permit(Common.GameTrigger.ScoreCalculationFinished, SingleGameState.End) // Let the UI know we are waiting for the going out score to be calculated. .OnEntry(() => this.FireEvent(this.CalculatingGoingOutScoreStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "CalculatingGoingOutScoreStarted")); }
private void ConfigureTrickTakingPhase(SingleGameProperties properties) { this.stateMachine.Configure(SingleGameState.TrickTaking) .Permit(Common.GameTrigger.ReadyForTrickTaking, SingleGameState.TrickTaking_WaitingForCurrentPlayer) .OnEntry(() => this.FireEvent(this.TrickTakingStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "TrickTakingStarted")); this.stateMachine.Configure(SingleGameState.TrickTaking_WaitingForCurrentPlayer) .SubstateOf(SingleGameState.TrickTaking) .Permit(Common.GameTrigger.CardPlaced, SingleGameState.TrickTaking_ValidatingCard) // Let the UI know we are waiting for the current player to place a card. .OnEntry(() => this.FireEvent(this.WaitingForCardStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "WaitingForCardStarted")); this.stateMachine.Configure(SingleGameState.TrickTaking_ValidatingCard) .SubstateOf(SingleGameState.TrickTaking) .Permit(Common.GameTrigger.WinningCardPlaced, SingleGameState.TrickTaking_RememberingWinningPlayer) .Permit(Common.GameTrigger.LosingCardPlaced, SingleGameState.TrickTaking_SwitchingToNextPlayer) .Permit(Common.GameTrigger.InvalidCardPlaced, SingleGameState.TrickTaking_RevertingInvalidMove) // Let the UI know we are waiting for the card to be validated. .OnEntry(() => this.FireEvent(this.ValidatingCardStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "ValidatingCardStarted")); this.stateMachine.Configure(SingleGameState.TrickTaking_RememberingWinningPlayer) .SubstateOf(SingleGameState.TrickTaking) .Permit(Common.GameTrigger.Internal, SingleGameState.TrickTaking_SwitchingToNextPlayer) .OnEntry(() => { properties.TrickWinnerPosition = properties.CurrentPlayerPosition; // Automatically switch to the next state. this.stateMachine.Fire(Common.GameTrigger.Internal); }); this.stateMachine.Configure(SingleGameState.TrickTaking_SwitchingToNextPlayer) .SubstateOf(SingleGameState.TrickTaking) .Permit(Common.GameTrigger.PlayerSwitched, SingleGameState.TrickTaking_WaitingForCurrentPlayer) .Permit(Common.GameTrigger.Internal, SingleGameState.TrickTaking_StartingNewRound) .OnEntry(() => { properties.CurrentPlayerPosition = (properties.CurrentPlayerPosition + 1) % properties.NumberOfPlayers; properties.RemainingCards--; // If all players placed a card, start a new round if (properties.RemainingCards % properties.NumberOfPlayers == 0) { this.stateMachine.Fire(Common.GameTrigger.Internal); } else { // Let the UI know we are waiting for the player who is allowed to place a card to be switched. this.FireEvent(this.SwitchingCurrentTrickPlayerStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "SwitchingCurrentTrickPlayerStarted"); } }); this.stateMachine.Configure(SingleGameState.TrickTaking_RevertingInvalidMove) .SubstateOf(SingleGameState.TrickTaking) .Permit(Common.GameTrigger.RevertingFinished, SingleGameState.TrickTaking_WaitingForCurrentPlayer) // Let the UI know we are waiting for an invalid move to be reverted. .OnEntry(() => this.FireEvent(this.RevertingInvalidMoveStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "RevertingInvalidMoveStarted")); this.stateMachine.Configure(SingleGameState.TrickTaking_StartingNewRound) .SubstateOf(SingleGameState.TrickTaking) .Permit(Common.GameTrigger.NewRoundStarted, SingleGameState.TrickTaking_WaitingForCurrentPlayer) .Permit(Common.GameTrigger.Internal, SingleGameState.CountingGameScore) .OnEntry(() => { // Whoever won the trick is now allowed to place the first card in the next roud. properties.CurrentPlayerPosition = properties.TrickWinnerPosition; // If there are no more cards left, end the game. if (properties.RemainingCards <= 0) { this.stateMachine.Fire(Common.GameTrigger.Internal); } else { // Let the UI know we are waiting for a new round to be started this.FireEvent(this.StartingNewRoundStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "StartingNewRoundStarted"); } }); }
private void ConfigureBiddingPhase(SingleGameProperties properties) { // Bidding phase this.stateMachine.Configure(SingleGameState.Bidding_WaitingForFirstBid) .SubstateOf(SingleGameState.Bidding) .Permit(Common.GameTrigger.BidPlaced, SingleGameState.Bidding_WaitingForNextPlayer) .Permit(Common.GameTrigger.Passed, SingleGameState.Bidding_SwitchingFirstBidPlayer) // Let listeners know we are waiting for a player to either make the first bid or pass. .OnEntry(() => this.FireEvent(this.WaitingForFirstBidStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "WaitingForFirstBidStarted")); this.stateMachine.Configure(SingleGameState.Bidding_SwitchingFirstBidPlayer) .SubstateOf(SingleGameState.Bidding) .Permit(Common.GameTrigger.PlayerSwitched, SingleGameState.Bidding_WaitingForFirstBid) .Permit(Common.GameTrigger.Internal, SingleGameState.ExchangingCardsWithTheDabb) .OnEntry(() => { properties.CurrentPlayerPosition = properties.NextPlayerPosition; properties.NextPlayerPosition = (properties.CurrentPlayerPosition + 1) % properties.NumberOfPlayers; // If the dealer is the current player and there still is no bid, the dealer automatically wins the round for 0 points // (this is extremely rare and not clearly defined in any rules) if (properties.CurrentPlayerPosition == properties.DealerPosition) { this.stateMachine.Fire(Common.GameTrigger.Internal); } else { // Let listeners know we are waiting for the UI to perform a player switch. this.FireEvent(this.SwitchingPlayerBeforeFirstBidStarted, new PlayerPairEventArgs(properties.CurrentPlayerPosition, properties.NextPlayerPosition), "SwitchingPlayerBeforeFirstBidStarted"); } }); this.stateMachine.Configure(SingleGameState.Bidding_WaitingForNextPlayer) .SubstateOf(SingleGameState.Bidding) .Permit(Common.GameTrigger.BidCountered, SingleGameState.Bidding_WaitingForCurrentPlayer) .Permit(Common.GameTrigger.Passed, SingleGameState.Bidding_SwitchingNextPlayer) // Let the UI know we are waiting for the next player to either counter bid or pass. .OnEntry(() => this.FireEvent(this.WaitingForCounterOrPassStarted, new PlayerPositionEventArgs(properties.NextPlayerPosition), "WaitingForCounterOrPassStarted")); this.stateMachine.Configure(SingleGameState.Bidding_WaitingForCurrentPlayer) .SubstateOf(SingleGameState.Bidding) .Permit(Common.GameTrigger.BidPlaced, SingleGameState.Bidding_WaitingForNextPlayer) .Permit(Common.GameTrigger.Passed, SingleGameState.Bidding_SwitchingCurrentPlayer) // Let the UI know we are waiting for the current player to either increase their bid (i.e. counter the next player) or pass. .OnEntry(() => this.FireEvent(this.WaitingForBidOrPassStarted, new PlayerPositionEventArgs(properties.CurrentPlayerPosition), "WaitingForBidOrPassStarted")); this.stateMachine.Configure(SingleGameState.Bidding_SwitchingCurrentPlayer) .SubstateOf(SingleGameState.Bidding) .Permit(Common.GameTrigger.PlayerSwitched, SingleGameState.Bidding_WaitingForNextPlayer) .Permit(Common.GameTrigger.Internal, SingleGameState.ExchangingCardsWithTheDabb) .OnEntry(() => { properties.CurrentPlayerPosition = properties.NextPlayerPosition; properties.NextPlayerPosition = (properties.CurrentPlayerPosition + 1) % properties.NumberOfPlayers; // if the new current player is the dealer, this means the dealer won the bidding round since the dealer countered the previous bid // and whoever placed that bid passed. if (properties.CurrentPlayerPosition == properties.DealerPosition) { this.stateMachine.Fire(Common.GameTrigger.Internal); } else { // Let the UI know we are waiting for the current and next players to be shifted counterclockwise. this.FireEvent(this.SwitchingCurrentBidPlayerStarted, new PlayerPairEventArgs(properties.CurrentPlayerPosition, properties.NextPlayerPosition), "SwitchingCurrentBidPlayerStarted"); } }); this.stateMachine.Configure(SingleGameState.Bidding_SwitchingNextPlayer) .SubstateOf(SingleGameState.Bidding) .Permit(Common.GameTrigger.PlayerSwitched, SingleGameState.Bidding_WaitingForNextPlayer) .Permit(Common.GameTrigger.Internal, SingleGameState.ExchangingCardsWithTheDabb) .OnEntry(() => { properties.NextPlayerPosition = (properties.NextPlayerPosition + 1) % properties.NumberOfPlayers; // if the new next player would be the player right of the dealer, this means the current player won the bidding roudn // since every player after them (and before them) passed. if (properties.NextPlayerPosition == (properties.DealerPosition + 1) % properties.NumberOfPlayers) { this.stateMachine.Fire(Common.GameTrigger.Internal); } else { // Let the UI know we are waiting for the next player to be switched. this.FireEvent(this.SwitchingCounterBidPlayerStarted, new PlayerPositionEventArgs(properties.NextPlayerPosition), "SwitchingCounterBidPlayerStarted"); } }); }