namespace Faraon.Tests; using Faraon.Core.Models; using Faraon.Core.Engine; using Faraon.Core.AI; public class GameEngineTests { [Fact] public void StartGame_DealsCorrectNumberOfCards() { var engine = new GameEngine(seed: 42); var players = new List { new Player("Hráč 1", PlayerType.Human), new Player("AI 1", PlayerType.AI), new Player("AI 2", PlayerType.AI), new Player("AI 3", PlayerType.AI) }; var rules = new RuleOptions { InitialCardCount = 5 }; engine.StartGame(players, rules); Assert.Equal(4, engine.State.Players.Count); Assert.All(engine.State.Players, p => Assert.Equal(5, p.Hand.Count)); Assert.Single(engine.State.DiscardPile); Assert.Equal(11, engine.State.DrawDeck.Count); } [Fact] public void Seven_IncreasesPendingDrawCount() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions { StackSevens = true }; engine.StartGame(new List { p1, p2 }, rules); p1.Hand.Clear(); var sevenCard = new Card(Suit.Hearts, Rank.Seven); p1.Hand.Add(sevenCard); p1.Hand.Add(new Card(Suit.Hearts, Rank.Eight)); // extra card so p1 does not finish engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Ten)); var played = engine.PlayCard(p1, sevenCard); Assert.True(played); Assert.Equal(3, engine.State.PendingDrawCount); Assert.Equal(p2, engine.State.CurrentPlayer); } [Fact] public void Ace_SetsPendingSkip() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions { CounterAceWithAce = true }; engine.StartGame(new List { p1, p2 }, rules); p1.Hand.Clear(); var aceCard = new Card(Suit.Bells, Rank.Ace); p1.Hand.Add(aceCard); p1.Hand.Add(new Card(Suit.Bells, Rank.Eight)); // extra card engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Bells, Rank.Nine)); var played = engine.PlayCard(p1, aceCard); Assert.True(played); Assert.True(engine.State.PendingSkip); Assert.Equal(p2, engine.State.CurrentPlayer); // Hráč 2 akceptuje státie engine.DrawOrAcknowledge(p2); Assert.False(engine.State.PendingSkip); Assert.Equal(p1, engine.State.CurrentPlayer); } [Fact] public void Hornik_ChangesSuitSuccessfully() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions(); engine.StartGame(new List { p1, p2 }, rules); p1.Hand.Clear(); var hornik = new Card(Suit.Acorns, Rank.OverKnave); p1.Hand.Add(hornik); p1.Hand.Add(new Card(Suit.Leaves, Rank.Eight)); // extra card engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Eight)); var played = engine.PlayCard(p1, hornik, Suit.Leaves); Assert.True(played); Assert.Equal(Suit.Leaves, engine.State.ActiveSuit); } [Fact] public void RedSeven_RevivesFinishedPlayer() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var p3 = new Player("Hráč 3", PlayerType.Human); var rules = new RuleOptions { RedSevenRevives = true }; engine.StartGame(new List { p1, p2, p3 }, rules); p3.Hand.Clear(); p3.FinishedPosition = 1; engine.State.LastFinishedPlayer = p3; engine.State.ReviveTurnsRemaining = 4; engine.State.ReviveRoundsRemaining = 2; p1.Hand.Clear(); var redSeven = new Card(Suit.Hearts, Rank.Seven); p1.Hand.Add(redSeven); p1.Hand.Add(new Card(Suit.Hearts, Rank.Eight)); engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Ten)); var played = engine.PlayCard(p1, redSeven); Assert.True(played); Assert.False(p3.HasFinished); Assert.Null(p3.FinishedPosition); Assert.Equal(3, p3.Hand.Count); } [Fact] public void RedSeven_AnyCardOneRound_CanPlayOnAnyCard() { var rules = new RuleOptions { RedSevenMode = RedSevenMode.AnyCardOneRound }; var redSeven = new Card(Suit.Hearts, Rank.Seven); var topCard = new Card(Suit.Acorns, Rank.King); // completely different suit and rank var canPlay = ActionValidator.CanPlayCard(redSeven, topCard, null, 0, false, rules); Assert.True(canPlay); } [Fact] public void RedSeven_ColorOrSeven_OnlyOnMatchingSuitOrSeven() { var rules = new RuleOptions { RedSevenMode = RedSevenMode.ColorOrSevenThreeRounds }; var redSeven = new Card(Suit.Hearts, Rank.Seven); var topAcornsKing = new Card(Suit.Acorns, Rank.King); var topHeartsKing = new Card(Suit.Hearts, Rank.King); var topBellsSeven = new Card(Suit.Bells, Rank.Seven); Assert.False(ActionValidator.CanPlayCard(redSeven, topAcornsKing, null, 0, false, rules)); Assert.True(ActionValidator.CanPlayCard(redSeven, topHeartsKing, null, 0, false, rules)); Assert.True(ActionValidator.CanPlayCard(redSeven, topBellsSeven, null, 0, false, rules)); } [Fact] public void RedSeven_ReviveExpiresAfterTurns() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var p3 = new Player("Hráč 3", PlayerType.Human); var rules = new RuleOptions { RedSevenMode = RedSevenMode.AnyCardOneRound }; engine.StartGame(new List { p1, p2, p3 }, rules); p3.Hand.Clear(); p3.Hand.Add(new Card(Suit.Leaves, Rank.Eight)); engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Leaves, Rank.Seven)); // p3 finishes engine.State.CurrentPlayerIndex = 2; var played = engine.PlayCard(p3, p3.Hand[0]); Assert.True(played); Assert.True(p3.HasFinished); Assert.Equal(p3, engine.State.LastFinishedPlayer); Assert.True(engine.State.ReviveTurnsRemaining > 0); // Advance turns until window expires while (engine.State.ReviveTurnsRemaining > 0) { engine.DrawOrAcknowledge(engine.State.CurrentPlayer); } Assert.Null(engine.State.LastFinishedPlayer); Assert.Equal(0, engine.State.ReviveTurnsRemaining); } [Fact] public void FourOfAKind_BurnsAndAllowsAnotherMove() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions { FourOfAKindBurns = true }; engine.StartGame(new List { p1, p2 }, rules); p1.Hand.Clear(); var fourKings = new List { new Card(Suit.Hearts, Rank.King), new Card(Suit.Leaves, Rank.King), new Card(Suit.Acorns, Rank.King), new Card(Suit.Bells, Rank.King) }; p1.Hand.AddRange(fourKings); p1.Hand.Add(new Card(Suit.Hearts, Rank.Eight)); var burned = engine.BurnCards(p1, fourKings); Assert.True(burned); Assert.Equal(p1, engine.State.CurrentPlayer); // Hráč pokračuje Assert.Single(p1.Hand); } [Fact] public void MultiSeven_StacksDoublePenalty() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions { StackSevens = true }; engine.StartGame(new List { p1, p2 }, rules); p1.Hand.Clear(); var sevens = new List { new Card(Suit.Hearts, Rank.Seven), new Card(Suit.Bells, Rank.Seven) }; p1.Hand.AddRange(sevens); p1.Hand.Add(new Card(Suit.Hearts, Rank.Eight)); // extra card engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Ten)); var played = engine.PlayCards(p1, sevens); Assert.True(played); Assert.Equal(6, engine.State.PendingDrawCount); // 2x sedma = +6 kariet Assert.Equal(p2, engine.State.CurrentPlayer); } [Fact] public void MultiCards_PlaysBatchOfSameRank() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions(); engine.StartGame(new List { p1, p2 }, rules); p1.Hand.Clear(); var dolniks = new List { new Card(Suit.Hearts, Rank.UnderKnave), new Card(Suit.Leaves, Rank.UnderKnave), new Card(Suit.Bells, Rank.UnderKnave) }; p1.Hand.AddRange(dolniks); p1.Hand.Add(new Card(Suit.Hearts, Rank.Eight)); // extra card engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Nine)); var played = engine.PlayCards(p1, dolniks); Assert.True(played); Assert.Single(p1.Hand); // Ostaná len 1 karta Assert.Equal(4, engine.State.DiscardPile.Count); Assert.Equal(p2, engine.State.CurrentPlayer); } [Fact] public void MultiCards_PreservesPlayerChosenOrder_AndSetsTopCard() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions(); engine.StartGame(new List { p1, p2 }, rules); p1.Hand.Clear(); var card1 = new Card(Suit.Hearts, Rank.Eight); // Matches table Hearts var card2 = new Card(Suit.Bells, Rank.Eight); // Desired top card (Bells) p1.Hand.Add(card1); p1.Hand.Add(card2); p1.Hand.Add(new Card(Suit.Acorns, Rank.Nine)); // extra engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Ten)); // Player explicitly chooses: card1 first, card2 second (so card2 becomes top of discard pile) var sequence = new List { card1, card2 }; var played = engine.PlayCards(p1, sequence); Assert.True(played); Assert.Equal(card2, engine.State.TopCard); // Card2 is on top Assert.NotNull(engine.State.TopCard); Assert.Equal(Suit.Bells, engine.State.TopCard.Suit); } [Fact] public void FaraonAI_MakesValidDecisions() { var engine = new GameEngine(seed: 42); var p1 = new Player("AI Bot", PlayerType.AI, difficulty: AIDifficulty.Hard); var p2 = new Player("AI Bot 2", PlayerType.AI, difficulty: AIDifficulty.Hard); engine.StartGame(new List { p1, p2 }, new RuleOptions()); var ai = new FaraonAI(); var decision = ai.MakeDecision(engine.State, p1); Assert.NotNull(decision); if (decision.Type == AIDecisionType.PlayCard) { Assert.NotNull(decision.Card); Assert.True(engine.CanPlayCard(p1, decision.Card)); } } [Fact] public void OlympicMode_LoserLosesOneCard_AndStartsNextRound() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var p3 = new Player("Hráč 3", PlayerType.Human); var rules = new RuleOptions { OlympicMode = true, InitialCardCount = 4 }; engine.StartGame(new List { p1, p2, p3 }, rules); // Hráč 1 vyhrá (prvý skončí) p1.Hand.Clear(); p1.Hand.Add(new Card(Suit.Hearts, Rank.Eight)); engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Seven)); engine.PlayCard(p1, p1.Hand[0]); // p1 finished 1st // Hráč 2 skončí druhý p2.Hand.Clear(); p2.Hand.Add(new Card(Suit.Hearts, Rank.Nine)); engine.PlayCard(p2, p2.Hand[0]); // p2 finished 2nd // Hráč 3 ostal posledný (porazený kola) Assert.True(engine.State.IsRoundOver); Assert.Equal(TurnState.RoundOver, engine.State.TurnState); Assert.Equal(p3, engine.State.RoundLoser); Assert.Equal(3, p3.OlympicCardsCount); // 4 - 1 = 3 karty // Spustenie ďalšieho kola engine.StartNextRound(); Assert.Equal(2, engine.State.RoundNumber); Assert.Equal(4, p1.Hand.Count); Assert.Equal(4, p2.Hand.Count); Assert.Equal(3, p3.Hand.Count); // Hráč 3 má o 1 kartu menej } [Fact] public void OlympicMode_EliminatesPlayer_AndDeclaresWinner() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions { OlympicMode = true, InitialCardCount = 1 }; engine.StartGame(new List { p1, p2 }, rules); // Hráč 1 vyhrá kolo p1.Hand.Clear(); p1.Hand.Add(new Card(Suit.Hearts, Rank.Eight)); engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Hearts, Rank.Seven)); engine.PlayCard(p1, p1.Hand[0]); // Hráč 2 prehrá kolo -> klesne z 1 na 0 kariet -> eliminovaný! Assert.True(p2.IsEliminated); Assert.True(engine.State.IsMatchOver); Assert.Equal(p1, engine.State.MatchWinner); Assert.Equal(TurnState.GameOver, engine.State.TurnState); } [Fact] public void GreenUnderKnave_CancelsSevenPenalty() { var engine = new GameEngine(seed: 42); var p1 = new Player("Hráč 1", PlayerType.Human); var p2 = new Player("Hráč 2", PlayerType.Human); var rules = new RuleOptions { GreenUnderKnaveMode = GreenUnderKnaveMode.OnlyOnSevenOrUnderKnave }; engine.StartGame(new List { p1, p2 }, rules); // p1 plays 7 -> penalty 3 p1.Hand.Clear(); p1.Hand.Add(new Card(Suit.Bells, Rank.Seven)); p1.Hand.Add(new Card(Suit.Leaves, Rank.Eight)); engine.State.DiscardPile.Clear(); engine.State.DiscardPile.Add(new Card(Suit.Bells, Rank.King)); engine.PlayCard(p1, p1.Hand[0]); Assert.Equal(3, engine.State.PendingDrawCount); // p2 plays Green Under Knave -> penalty becomes 0 p2.Hand.Clear(); var greenUnderKnave = new Card(Suit.Leaves, Rank.UnderKnave); p2.Hand.Add(greenUnderKnave); p2.Hand.Add(new Card(Suit.Hearts, Rank.Nine)); var played = engine.PlayCard(p2, greenUnderKnave); Assert.True(played); Assert.Equal(0, engine.State.PendingDrawCount); Assert.Equal(greenUnderKnave, engine.State.TopCard); } [Fact] public void GreenUnderKnave_UniversalWildcard_AllowsAnyPlay() { var rules = new RuleOptions { GreenUnderKnaveMode = GreenUnderKnaveMode.UniversalWildcard }; var greenUnderKnave = new Card(Suit.Leaves, Rank.UnderKnave); var topAcornsKing = new Card(Suit.Acorns, Rank.King); var arbitraryCard = new Card(Suit.Hearts, Rank.Ten); // Green Under Knave can be played on Acorns King Assert.True(ActionValidator.CanPlayCard(greenUnderKnave, topAcornsKing, null, 0, false, rules)); // Arbitrary card (Hearts Ten) can be played on Green Under Knave Assert.True(ActionValidator.CanPlayCard(arbitraryCard, greenUnderKnave, null, 0, false, rules)); } [Fact] public void GreenUnderKnave_Disabled_DoesNotCancelPenalty() { var rules = new RuleOptions { GreenUnderKnaveMode = GreenUnderKnaveMode.Disabled }; var greenUnderKnave = new Card(Suit.Leaves, Rank.UnderKnave); var topSeven = new Card(Suit.Hearts, Rank.Seven); // When facing penalty and disabled, Green Under Knave cannot be played Assert.False(ActionValidator.CanPlayCard(greenUnderKnave, topSeven, null, 3, false, rules)); } [Fact] public void AIDialogueService_ReturnsAppropriateDialogues() { var easyAI = new Player("AI Easy", PlayerType.AI, "🦊", AIDifficulty.Easy); var hardAI = new Player("AI Hard", PlayerType.AI, "🤖", AIDifficulty.Hard); var state = new GameState(); easyAI.Hand.Add(new Card(Suit.Hearts, Rank.Eight)); easyAI.Hand.Add(new Card(Suit.Hearts, Rank.Nine)); // Thinking dialogue for easy AI var easyThought = AIDialogueService.GetThinkingDialogue(easyAI, state); Assert.False(string.IsNullOrWhiteSpace(easyThought)); // Thinking dialogue for hard AI var hardThought = AIDialogueService.GetThinkingDialogue(hardAI, state); Assert.False(string.IsNullOrWhiteSpace(hardThought)); // Event reactions var freezeEvent = new GameEvent(GameEventType.AceSkipped, easyAI); var freezeReaction = AIDialogueService.GetEventReaction(freezeEvent, easyAI); Assert.False(string.IsNullOrWhiteSpace(freezeReaction)); var penaltyEvent = new GameEvent(GameEventType.SevenStacked, null, TargetPlayer: easyAI, ExtraData: 6); var penaltyReaction = AIDialogueService.GetEventReaction(penaltyEvent, easyAI); Assert.False(string.IsNullOrWhiteSpace(penaltyReaction)); var cancelEvent = new GameEvent(GameEventType.SevenCanceled, easyAI); var cancelReaction = AIDialogueService.GetEventReaction(cancelEvent, easyAI); Assert.False(string.IsNullOrWhiteSpace(cancelReaction)); // Card jokes var multiDrawJoke = AIDialogueService.GetContextualJoke(GameEventType.SevenStacked, 12, 10); Assert.NotNull(multiDrawJoke); Assert.False(string.IsNullOrWhiteSpace(multiDrawJoke.Punchline)); var burnJoke = AIDialogueService.GetContextualJoke(GameEventType.CardsBurned, 0, 10); Assert.NotNull(burnJoke); Assert.False(string.IsNullOrWhiteSpace(burnJoke.Setup)); var longGameJoke = AIDialogueService.GetContextualJoke(null, 0, 32); Assert.NotNull(longGameJoke); Assert.False(string.IsNullOrWhiteSpace(longGameJoke.Punchline)); } [Fact] public void AIPersonality_ChangesLiveAvatar_BasedOnEmotion() { var fox = AIPersonality.Fox; Assert.Equal("fox", fox.Id); Assert.Equal("🦊", fox.GetAvatarForEmotion(PlayerEmotion.Neutral)); Assert.Equal("🧐", fox.GetAvatarForEmotion(PlayerEmotion.Thinking)); Assert.Equal("🙀", fox.GetAvatarForEmotion(PlayerEmotion.Shocked)); Assert.Equal("🥶", fox.GetAvatarForEmotion(PlayerEmotion.Freezing)); Assert.Equal("😾", fox.GetAvatarForEmotion(PlayerEmotion.Angry)); Assert.Equal("🥳", fox.GetAvatarForEmotion(PlayerEmotion.Celebrating)); var player = new Player(fox.Name, PlayerType.AI, fox.BaseAvatar, fox.Difficulty, fox.Id); Assert.Equal("🦊", player.GetLiveAvatar()); player.CurrentEmotion = PlayerEmotion.Shocked; Assert.Equal("🙀", player.GetLiveAvatar()); Assert.Equal("💧", player.GetEmotionBadge()); player.CurrentEmotion = PlayerEmotion.Freezing; Assert.Equal("🥶", player.GetLiveAvatar()); Assert.Equal("❄️", player.GetEmotionBadge()); player.CurrentEmotion = PlayerEmotion.Angry; Assert.Equal("😾", player.GetLiveAvatar()); Assert.Equal("🔥", player.GetEmotionBadge()); } }