# Tutorial: Building a 9-Card Magic Trick Game ## Overview In this tutorial, you'll learn how to extend the CardsStarterKit framework to create a simple interactive magic trick called the "9-Card Mind Reader." This trick teaches you the fundamentals of: - Extending the `CardsGame` base class - Creating custom game rules with `GameRule` - Managing game state machines - Working with card animations - Creating interactive UI with buttons - Handling single-player gameplay **Target Audience:** MonoGame developers new to card games **Estimated Time:** 2-3 hours **Difficulty:** Beginner --- ## The Magic Trick Explained ### How It Works The 9-Card Mind Reader is a classic mathematical card trick: 1. **Setup:** Lay out 9 cards face-up in a 3x3 grid 2. **Selection:** The spectator (player) mentally selects one card 3. **Reveal Phase 1:** The magician picks up the cards in 3 columns and asks "Which pile is your card in?" 4. **Rearrange:** The magician places the selected pile in the middle and lays out the cards again in 3 columns 5. **Reveal Phase 2:** The magician asks again "Which pile?" 6. **Final Reveal:** The magician dramatically reveals the middle card - which is always the selected card! ### The Secret By placing the selected pile in the middle position twice, the card always ends up in the center position (5th card). Simple mathematics makes it foolproof! --- ## Part 1: Project Structure Setup ### Step 1.1: Create the Directory Structure We'll organize our magic trick code similarly to the Blackjack implementation: ``` 3-Games/MagicTrick/ ├── Core/ │ ├── MagicTrickCardGame.cs │ ├── MagicTrickGameState.cs ├── Players/ │ └── MagicTrickPlayer.cs ├── Rules/ │ ├── CardSelectionRule.cs │ └── RevealRule.cs └── UI/ └── (We'll use existing Button class from Blackjack) ``` Create these directories: ```bash mkdir -p 3-Games/MagicTrick/Core mkdir -p 3-Games/MagicTrick/Players mkdir -p 3-Games/MagicTrick/Rules mkdir -p 3-Games/MagicTrick/UI ``` --- ## Part 2: Define Game State ### Step 2.1: Create the Game State Enum The magic trick has distinct phases, so we'll use a state machine to control flow. **Create:** `3-Games/MagicTrick/Core/MagicTrickGameState.cs` ```csharp namespace MagicTrick { /// /// Represents the various states of the magic trick game /// public enum MagicTrickGameState { /// /// Initial setup - dealing 9 cards /// Dealing, /// /// Player is selecting a card mentally /// PlayerSelecting, /// /// First pile selection phase /// FirstPileSelection, /// /// Cards being rearranged after first selection /// FirstRearrange, /// /// Second pile selection phase /// SecondPileSelection, /// /// Final rearrangement /// SecondRearrange, /// /// Revealing the selected card /// Revealing, /// /// Trick complete - show result /// Complete } } ``` **Why This Approach?** - Each state represents a distinct phase of the trick - Makes the game loop clear and maintainable - Easy to add animations and UI changes per state - Follows the same pattern as BlackjackGameState --- ## Part 3: Create the Player Class ### Step 3.1: Define MagicTrickPlayer Our player needs minimal state - just tracking which pile they selected. **Create:** `3-Games/MagicTrick/Players/MagicTrickPlayer.cs` ```csharp using CardsFramework.Players; using CardsFramework.Game; namespace MagicTrick { /// /// Represents the spectator in the magic trick /// public class MagicTrickPlayer : Player { /// /// Which pile (0, 1, or 2) the player selected in the current round /// public int SelectedPile { get; set; } /// /// Whether the player has made their selection /// public bool HasSelected { get; set; } /// /// Creates a new magic trick player /// /// Player's name /// Reference to the game instance public MagicTrickPlayer(string name, CardsGame game) : base(name, game) { SelectedPile = -1; HasSelected = false; } /// /// Resets the player state for a new trick /// public void ResetSelection() { SelectedPile = -1; HasSelected = false; } } } ``` **Key Points:** - Extends `Player` base class (gives us Name, Game, Hand) - `SelectedPile`: Tracks which of the 3 piles contains their card (0=left, 1=middle, 2=right) - `HasSelected`: Simple flag to prevent double-selection - `ResetSelection()`: Prepares for a new trick --- ## Part 4: Create Game Rules ### Step 4.1: Card Selection Rule This rule checks if the player has made their pile selection and triggers the next phase. **Create:** `3-Games/MagicTrick/Rules/CardSelectionRule.cs` ```csharp using System; using CardsFramework.Rules; namespace MagicTrick { /// /// Event arguments for card selection events /// public class CardSelectionEventArgs : EventArgs { public MagicTrickPlayer Player { get; set; } public int SelectedPile { get; set; } } /// /// Rule that fires when the player selects a pile /// public class CardSelectionRule : GameRule { private readonly MagicTrickPlayer player; private bool previousHasSelected; public CardSelectionRule(MagicTrickPlayer player) { this.player = player; this.previousHasSelected = false; } /// /// Checks if the player has made a new selection /// public override void Check() { // Only fire event when selection changes from false to true if (player.HasSelected && !previousHasSelected) { previousHasSelected = true; // Fire the event FireRuleMatch(new CardSelectionEventArgs { Player = player, SelectedPile = player.SelectedPile }); } // Reset tracking when starting a new selection phase if (!player.HasSelected) { previousHasSelected = false; } } } } ``` **How It Works:** - Inherits from `GameRule` base class - Tracks previous state to detect state changes - Fires `RuleMatch` event only when selection transitions from false → true - Resets when player starts a new selection phase - Event includes which pile was selected ### Step 4.2: Reveal Rule This rule determines when we're ready to reveal the selected card. **Create:** `3-Games/MagicTrick/Rules/RevealRule.cs` ```csharp using System; using CardsFramework.Rules; using CardsFramework.Cards; namespace MagicTrick { /// /// Event arguments for reveal events /// public class RevealEventArgs : EventArgs { public TraditionalCard RevealedCard { get; set; } } /// /// Rule that fires when it's time to reveal the selected card /// public class RevealRule : GameRule { private readonly MagicTrickCardGame game; private bool hasRevealed; public RevealRule(MagicTrickCardGame game) { this.game = game; this.hasRevealed = false; } /// /// Checks if we're in the revealing state /// public override void Check() { if (game.State == MagicTrickGameState.Revealing && !hasRevealed) { hasRevealed = true; // The 5th card (index 4) is always the selected card after two rounds if (game.TableCards.Count >= 5) { FireRuleMatch(new RevealEventArgs { RevealedCard = game.TableCards[4] }); } } // Reset for next trick if (game.State == MagicTrickGameState.Dealing) { hasRevealed = false; } } } } ``` **Key Points:** - Fires when game enters `Revealing` state - The magic happens here: `TableCards[4]` is always at index 4 after two rearrangements - One-shot rule: Only fires once until reset - Passes the revealed card to event handlers --- ## Part 5: Create the Main Game Class ### Step 5.1: MagicTrickCardGame - Part 1 (Fields and Constructor) This is the core of our implementation. We'll build it in sections. **Create:** `3-Games/MagicTrick/Core/MagicTrickCardGame.cs` ```csharp using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using CardsFramework.Cards; using CardsFramework.Game; using CardsFramework.Players; using CardsFramework.UI; using CardsFramework.Rules; using CardsFramework.Core; namespace MagicTrick { /// /// Main game class for the 9-card magic trick /// public class MagicTrickCardGame : CardsGame { #region Fields // Game state private MagicTrickGameState currentState; public MagicTrickGameState State { get { return currentState; } set { currentState = value; } } // The 9 cards currently on the table public List TableCards { get; private set; } // Animated components for displaying cards in 3x3 grid private List animatedCards; // UI Components private Button buttonPile1; private Button buttonPile2; private Button buttonPile3; private Button buttonContinue; private Button buttonNewTrick; // The player (spectator) private MagicTrickPlayer player; // Game rules private CardSelectionRule cardSelectionRule; private RevealRule revealRule; // Display text for instructions private string instructionText; private Vector2 instructionPosition; private SpriteFont instructionFont; // Card layout constants private const int CardsPerRow = 3; private const int TotalCards = 9; private const float CardSpacingX = 120f; private const float CardSpacingY = 160f; private Vector2 gridStartPosition; // State transition timer private float stateTransitionTimer; private float stateTransitionDelay; private MagicTrickGameState nextState; private bool waitingForStateTransition; #endregion #region Initialization private ScreenManager screenManager; /// /// Creates a new magic trick game /// /// The game table for layout /// The screen manager for SpriteBatch and Content public MagicTrickCardGame(GameTable gameTable, ScreenManager screenManager) : base( decks: 1, // Only need 1 deck jokersInDeck: 0, // No jokers suits: CardSuit.AllSuits, // All suits available cardValues: CardValue.NonJokers, // Standard cards only minimumPlayers: 1, // Single player maximumPlayers: 1, // Single player gameTable: gameTable, theme: "Default") { this.screenManager = screenManager; TableCards = new List(); animatedCards = new List(); instructionText = ""; } /// /// Initializes the game components /// public override void Initialize() { base.Initialize(); // Calculate grid start position (centered on screen) int screenWidth = GraphicsDevice.Viewport.Width; int screenHeight = GraphicsDevice.Viewport.Height; gridStartPosition = new Vector2( (screenWidth - (CardSpacingX * (CardsPerRow - 1))) / 2 - 50, 100 ); instructionPosition = new Vector2(screenWidth / 2, 50); // Start with dealing state currentState = MagicTrickGameState.Dealing; } /// /// Loads content and creates UI components /// public override void LoadContent() { base.LoadContent(); // Load instruction font instructionFont = screenManager.Font; // Get input state from screenManager InputState input = new InputState(); // Create buttons using the actual Button constructor // Buttons use texture names, not Texture2D objects // Pile 1 button (left) buttonPile1 = new Button( "ButtonRegular", // Regular texture "ButtonPressed", // Pressed texture input, // Input state this, // CardsGame screenManager.SpriteBatch, screenManager.GlobalTransformation ); buttonPile1.Click += ButtonPile1_Click; buttonPile1.Visible = false; Game.Components.Add(buttonPile1); // Pile 2 button (middle) buttonPile2 = new Button( "ButtonRegular", "ButtonPressed", input, this, screenManager.SpriteBatch, screenManager.GlobalTransformation ); buttonPile2.Click += ButtonPile2_Click; buttonPile2.Visible = false; Game.Components.Add(buttonPile2); // Pile 3 button (right) buttonPile3 = new Button( "ButtonRegular", "ButtonPressed", input, this, screenManager.SpriteBatch, screenManager.GlobalTransformation ); buttonPile3.Click += ButtonPile3_Click; buttonPile3.Visible = false; Game.Components.Add(buttonPile3); // Continue button buttonContinue = new Button( "ButtonRegular", "ButtonPressed", input, this, screenManager.SpriteBatch, screenManager.GlobalTransformation ); buttonContinue.Click += ButtonContinue_Click; buttonContinue.Visible = false; Game.Components.Add(buttonContinue); // New Trick button buttonNewTrick = new Button( "ButtonRegular", "ButtonPressed", input, this, screenManager.SpriteBatch, screenManager.GlobalTransformation ); buttonNewTrick.Click += ButtonNewTrick_Click; buttonNewTrick.Visible = false; Game.Components.Add(buttonNewTrick); } #endregion ``` **What We've Set Up:** - **Fields**: Track game state, cards, UI components - **Constructor**: Initializes with 1 deck, no jokers, single player - **Initialize**: Calculates card grid positioning - **LoadContent**: Creates 5 buttons (3 pile buttons, continue, new trick) ### Step 5.2: MagicTrickCardGame - Part 2 (Player Management) Add these methods to handle players: ```csharp #region Player Management /// /// Adds a player to the game /// public override void AddPlayer(Player newPlayer) { if (!(newPlayer is MagicTrickPlayer)) { throw new ArgumentException("Player must be of type MagicTrickPlayer"); } base.AddPlayer(newPlayer); player = (MagicTrickPlayer)newPlayer; // Initialize rules now that we have a player cardSelectionRule = new CardSelectionRule(player); cardSelectionRule.RuleMatch += CardSelectionRule_RuleMatch; Rules.Add(cardSelectionRule); revealRule = new RevealRule(this); revealRule.RuleMatch += RevealRule_RuleMatch; Rules.Add(revealRule); } /// /// Gets the current player /// public override Player GetCurrentPlayer() { return player; } #endregion ``` **Key Points:** - Validates player type - Initializes rules after player is added (rules need player reference) - Wires up rule event handlers ### Step 5.3: MagicTrickCardGame - Part 3 (Dealing Cards) ```csharp #region Card Management /// /// Deals 9 cards into a 3x3 grid /// public override void Deal() { // Clear previous cards ClearTable(); // Shuffle the deck dealer.Shuffle(); // Deal 9 cards to the table for (int i = 0; i < TotalCards; i++) { TraditionalCard card = dealer[i]; TableCards.Add(card); // Create animated component for this card AnimatedCardsGameComponent animatedCard = new AnimatedCardsGameComponent( card, this, screenManager.SpriteBatch, screenManager.GlobalTransformation ); animatedCard.LoadContent(); // Calculate position in 3x3 grid int row = i / CardsPerRow; int col = i % CardsPerRow; Vector2 targetPosition = gridStartPosition + new Vector2( col * CardSpacingX, row * CardSpacingY ); animatedCard.CurrentPosition = targetPosition; animatedCard.IsFaceDown = false; // Show cards face-up animatedCards.Add(animatedCard); Game.Components.Add(animatedCard); } } /// /// Clears all cards from the table /// private void ClearTable() { // Remove animated components foreach (var animatedCard in animatedCards) { Game.Components.Remove(animatedCard); } animatedCards.Clear(); TableCards.Clear(); } /// /// Rearranges cards after pile selection /// /// Which pile (0, 1, 2) contains the player's card private void RearrangeCards(int selectedPile) { // Collect cards by column (pile) List pile1 = new List(); // Left column List pile2 = new List(); // Middle column List pile3 = new List(); // Right column // Group cards into columns for (int i = 0; i < TableCards.Count; i++) { int col = i % CardsPerRow; if (col == 0) pile1.Add(TableCards[i]); else if (col == 1) pile2.Add(TableCards[i]); else pile3.Add(TableCards[i]); } // Rearrange: Put selected pile in the middle // This is the key to the trick! List rearranged = new List(); if (selectedPile == 0) // Left pile selected { rearranged.AddRange(pile2); // Other pile first rearranged.AddRange(pile1); // Selected pile in middle rearranged.AddRange(pile3); // Other pile last } else if (selectedPile == 1) // Middle pile selected { rearranged.AddRange(pile1); rearranged.AddRange(pile2); // Already in middle rearranged.AddRange(pile3); } else // Right pile selected { rearranged.AddRange(pile1); rearranged.AddRange(pile3); // Selected pile in middle rearranged.AddRange(pile2); } // Update table cards TableCards.Clear(); TableCards.AddRange(rearranged); // Redeal cards in new order RedealCards(); } /// /// Redeals cards to table in their current order /// private void RedealCards() { // Remove old animated components foreach (var animatedCard in animatedCards) { Game.Components.Remove(animatedCard); } animatedCards.Clear(); // Create new animated components in new positions for (int i = 0; i < TableCards.Count; i++) { AnimatedCardsGameComponent animatedCard = new AnimatedCardsGameComponent( TableCards[i], this, screenManager.SpriteBatch, screenManager.GlobalTransformation ); animatedCard.LoadContent(); // Calculate position in 3x3 grid int row = i / CardsPerRow; int col = i % CardsPerRow; Vector2 targetPosition = gridStartPosition + new Vector2( col * CardSpacingX, row * CardSpacingY ); animatedCard.CurrentPosition = targetPosition; animatedCard.IsFaceDown = false; animatedCards.Add(animatedCard); Game.Components.Add(animatedCard); } } #endregion ``` **The Magic Explained:** - `RearrangeCards()` is where the trick happens! - We collect cards by **column** (each column = a pile) - We place the selected pile in the **middle** of the new arrangement - After doing this **twice**, the selected card mathematically ends up at position 4 (center) ### Step 5.4: MagicTrickCardGame - Part 4 (Game Flow) ```csharp #region Game Flow /// /// Starts the trick /// public override void StartPlaying() { currentState = MagicTrickGameState.Dealing; Deal(); // Move to selection phase after brief delay ScheduleStateTransition(MagicTrickGameState.PlayerSelecting, 1000f); } /// /// Schedules a state transition after a delay /// /// The state to transition to /// Delay in milliseconds private void ScheduleStateTransition(MagicTrickGameState newState, float delayMs) { nextState = newState; stateTransitionDelay = delayMs; stateTransitionTimer = 0; waitingForStateTransition = true; } /// /// Updates the game state each frame /// public override void Update(GameTime gameTime) { base.Update(gameTime); // Handle state transition timer if (waitingForStateTransition) { stateTransitionTimer += (float)gameTime.ElapsedGameTime.TotalMilliseconds; if (stateTransitionTimer >= stateTransitionDelay) { currentState = nextState; waitingForStateTransition = false; } } // Check rules CheckRules(); // Update UI based on current state UpdateUIForState(); } /// /// Updates button visibility and instruction text based on state /// private void UpdateUIForState() { // Hide all buttons first buttonPile1.Visible = false; buttonPile2.Visible = false; buttonPile3.Visible = false; buttonContinue.Visible = false; buttonNewTrick.Visible = false; switch (currentState) { case MagicTrickGameState.Dealing: instructionText = "Watch carefully as the cards are dealt..."; break; case MagicTrickGameState.PlayerSelecting: instructionText = "Mentally select one card. Remember it!\nClick Continue when ready."; buttonContinue.Visible = true; break; case MagicTrickGameState.FirstPileSelection: instructionText = "Which pile contains your card?\n(Cards in each column are a pile)"; buttonPile1.Visible = true; buttonPile2.Visible = true; buttonPile3.Visible = true; break; case MagicTrickGameState.FirstRearrange: instructionText = "Watch as I rearrange the cards..."; break; case MagicTrickGameState.SecondPileSelection: instructionText = "Now, which pile contains your card?"; buttonPile1.Visible = true; buttonPile2.Visible = true; buttonPile3.Visible = true; break; case MagicTrickGameState.SecondRearrange: instructionText = "One more rearrangement..."; break; case MagicTrickGameState.Revealing: instructionText = "Your card is..."; break; case MagicTrickGameState.Complete: // Get the revealed card for display if (TableCards.Count >= 5) { TraditionalCard revealedCard = TableCards[4]; instructionText = $"Your card is the {revealedCard.Value} of {revealedCard.Type}!\n\nDid I guess correctly?"; } buttonNewTrick.Visible = true; break; } } #endregion ``` **State Machine Logic:** - Each state has specific UI configuration - Instructions guide the player through the trick - Buttons appear/hide based on what's needed - The state flows: Dealing → Selecting → FirstPile → Rearrange → SecondPile → Rearrange → Reveal → Complete ### Step 5.5: MagicTrickCardGame - Part 5 (Event Handlers) ```csharp #region Event Handlers /// /// Handles pile 1 (left) button click /// private void ButtonPile1_Click(object sender, EventArgs e) { SelectPile(0); } /// /// Handles pile 2 (middle) button click /// private void ButtonPile2_Click(object sender, EventArgs e) { SelectPile(1); } /// /// Handles pile 3 (right) button click /// private void ButtonPile3_Click(object sender, EventArgs e) { SelectPile(2); } /// /// Handles pile selection /// private void SelectPile(int pileIndex) { player.SelectedPile = pileIndex; player.HasSelected = true; // Rule will detect this change and fire event } /// /// Handles continue button click /// private void ButtonContinue_Click(object sender, EventArgs e) { if (currentState == MagicTrickGameState.PlayerSelecting) { currentState = MagicTrickGameState.FirstPileSelection; } } /// /// Handles new trick button click /// private void ButtonNewTrick_Click(object sender, EventArgs e) { player.ResetSelection(); StartPlaying(); } /// /// Handles card selection rule match /// private void CardSelectionRule_RuleMatch(object sender, EventArgs e) { CardSelectionEventArgs args = (CardSelectionEventArgs)e; if (currentState == MagicTrickGameState.FirstPileSelection) { // First selection made currentState = MagicTrickGameState.FirstRearrange; // Rearrange cards with selected pile in middle RearrangeCards(args.SelectedPile); // Reset for next selection player.ResetSelection(); // Move to second selection after delay ScheduleStateTransition(MagicTrickGameState.SecondPileSelection, 1500f); } else if (currentState == MagicTrickGameState.SecondPileSelection) { // Second selection made currentState = MagicTrickGameState.SecondRearrange; // Rearrange again RearrangeCards(args.SelectedPile); // Move to reveal after delay ScheduleStateTransition(MagicTrickGameState.Revealing, 1500f); } } /// /// Handles reveal rule match /// private void RevealRule_RuleMatch(object sender, EventArgs e) { RevealEventArgs args = (RevealEventArgs)e; // Highlight the revealed card (center card) if (animatedCards.Count >= 5) { // You could add a scale animation or glow effect here // For simplicity, we'll just move to complete state } // Move to complete state System.Threading.Tasks.Task.Delay(2000).ContinueWith(t => { currentState = MagicTrickGameState.Complete; }); } #endregion ``` **Event Flow:** 1. Player clicks pile button → `SelectPile()` → Updates player state 2. `CardSelectionRule` detects state change → Fires `RuleMatch` 3. `CardSelectionRule_RuleMatch()` → Rearranges cards → Advances state 4. After 2 selections → `RevealRule` fires → Shows result ### Step 5.6: MagicTrickCardGame - Part 6 (Drawing and Utilities) ```csharp #region Drawing /// /// Draws the game /// public override void Draw(GameTime gameTime) { base.Draw(gameTime); // Draw instruction text if (!string.IsNullOrEmpty(instructionText) && instructionFont != null) { SpriteBatch spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch)); if (spriteBatch != null) { // Measure text for centering Vector2 textSize = instructionFont.MeasureString(instructionText); Vector2 centeredPosition = new Vector2( instructionPosition.X - textSize.X / 2, instructionPosition.Y ); // Draw text with shadow for readability spriteBatch.DrawString(instructionFont, instructionText, centeredPosition + new Vector2(2, 2), Color.Black); spriteBatch.DrawString(instructionFont, instructionText, centeredPosition, Color.White); } } } #endregion #region Utilities /// /// Gets the value of a card (not used in magic trick, but required by base class) /// /// /// IMPORTANT: The CardsGame base class declares CardValue() as an abstract method, /// so every game MUST override it. Even though the magic trick doesn't need card /// values for scoring, we provide a standard implementation here. Games like Blackjack /// and Gin Rummy use this for actual scoring logic. /// public override int CardValue(TraditionalCard card) { // Magic trick doesn't need card values, but we implement for completeness // This is a standard card value mapping (Aces=1, Face cards=10) switch (card.Value) { case CardValue.Ace: return 1; case CardValue.Two: return 2; case CardValue.Three: return 3; case CardValue.Four: return 4; case CardValue.Five: return 5; case CardValue.Six: return 6; case CardValue.Seven: return 7; case CardValue.Eight: return 8; case CardValue.Nine: return 9; case CardValue.Ten: case CardValue.Jack: case CardValue.Queen: case CardValue.King: return 10; default: return 0; } } #endregion } } ``` **Drawing:** - Centers instruction text on screen - Adds shadow for readability - Cards draw themselves via `AnimatedCardsGameComponent` --- ## Part 6: Integrate with Screen System ### Step 6.1: Create a Screen for the Magic Trick **Create:** `2-Core/Screens/MagicTrickGameplayScreen.cs` ```csharp using System; using Microsoft.Xna.Framework; using CardsFramework.MagicTrick; using CardsFramework.UI; using CardsFramework.Core; namespace CardsStarterKit { /// /// Screen that hosts the magic trick game /// public class MagicTrickGameplayScreen : GameScreen { private MagicTrickCardGame magicTrickGame; public MagicTrickGameplayScreen() { EnabledGestures = Microsoft.Xna.Framework.Input.Touch.GestureType.Tap; } /// /// Loads content and initializes the game /// public override void LoadContent() { base.LoadContent(); // Create game table GameTable gameTable = new GameTable(ScreenManager.Game, 1); // 1 player position // Create the magic trick game magicTrickGame = new MagicTrickCardGame(gameTable); ScreenManager.Game.Components.Add(magicTrickGame); magicTrickGame.Initialize(); magicTrickGame.LoadContent(); // Add the player MagicTrickPlayer player = new MagicTrickPlayer("You", magicTrickGame); magicTrickGame.AddPlayer(player); // Start the trick magicTrickGame.StartPlaying(); } /// /// Handles input /// public override void HandleInput(InputState input) { base.HandleInput(input); // Handle back button / escape to return to menu if (input.IsPauseGame(null)) { ScreenManager.AddScreen(new PauseScreen(), null); } } /// /// Updates the screen /// public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); } /// /// Cleanup when exiting /// public override void UnloadContent() { if (magicTrickGame != null) { ScreenManager.Game.Components.Remove(magicTrickGame); } base.UnloadContent(); } } } ``` **Integration:** - Creates `GameTable` for layout - Instantiates `MagicTrickCardGame` - Adds single player - Starts the trick - Handles pause/back navigation ### Step 6.2: Add Menu Entry **Modify:** `2-Core/Screens/MainMenuScreen.cs` Find the constructor where menu entries are added and add: ```csharp // Add magic trick menu entry MenuEntry magicTrickMenuEntry = new MenuEntry("Magic Trick"); magicTrickMenuEntry.Selected += MagicTrickMenuEntrySelected; menuEntries.Add(magicTrickMenuEntry); ``` Then add the event handler method: ```csharp /// /// Handles Magic Trick menu selection /// private void MagicTrickMenuEntrySelected(object sender, EventArgs e) { ScreenManager.AddScreen(new MagicTrickGameplayScreen(), null); } ``` --- ## Part 7: Testing Your Magic Trick ### Step 7.1: Build and Run ```bash dotnet build # For macOS/Linux dotnet run --project Platforms/DesktopGL/CardsStarterKit.DesktopGL.csproj # For Windows dotnet run --project Platforms/WindowsDX/CardsStarterKit.WindowsDX.csproj ``` ### Step 7.2: Test the Flow 1. **Launch:** Select "Magic Trick" from main menu 2. **Deal:** 9 cards appear in a 3x3 grid 3. **Select:** Mentally pick a card (e.g., 7 of Hearts in top-right) 4. **First Question:** Click which pile (column) contains your card 5. **Rearrange:** Watch cards rearrange 6. **Second Question:** Again, click which pile contains your card 7. **Reveal:** The center card should be your selected card! ### Step 7.3: Verify the Math Try this test: - Pick the card at position [0,2] (top-right) - That's in pile 2 (right column) - After first rearrange, it should move - After second rearrange, it should be at index 4 (center) The math always works! --- ## Part 8: Enhancements (Optional) ### Enhancement 1: Add Card Highlighting In the `Revealing` state, highlight the center card: ```csharp // In RevealRule_RuleMatch method: if (animatedCards.Count >= 5) { AnimatedCardsGameComponent centerCard = animatedCards[4]; // Add scale animation to make it pulse ScaleGameComponentAnimation scaleAnim = new ScaleGameComponentAnimation(centerCard) { Duration = TimeSpan.FromSeconds(1), ScaleFactor = 1.2f, IsLooped = true, AnimationCycles = 3 }; centerCard.AnimationsList.Add(scaleAnim); } ``` ### Enhancement 2: Add Shuffle Animation Make the rearrangement more dramatic: ```csharp // In RearrangeCards method, before RedealCards(): // Animate cards flying off screen then back foreach (var animatedCard in animatedCards) { Vector2 offscreenPos = new Vector2(-500, animatedCard.CurrentPosition.Y); TransitionGameComponentAnimation transition = new TransitionGameComponentAnimation( animatedCard, offscreenPos, TimeSpan.FromSeconds(0.5) ); animatedCard.AnimationsList.Add(transition); } // Then delay RedealCards() until animation completes ``` ### Enhancement 3: Add Sound Effects ```csharp // In LoadContent: SoundEffect cardDealSound = Game.Content.Load(@"Sounds\CardPlace"); SoundEffect revealSound = Game.Content.Load(@"Sounds\Reveal"); // Play at appropriate times: cardDealSound.Play(); // When dealing revealSound.Play(); // When revealing ``` --- ## Key Takeaways ### What You Learned 1. **Extending CardsGame:** - Override `Deal()`, `AddPlayer()`, `GetCurrentPlayer()` - Use constructor to specify deck configuration - Implement state machines for game flow 2. **Creating Game Rules:** - Inherit from `GameRule` - Override `Check()` method - Fire `RuleMatch` events when conditions are met - Track state changes to prevent duplicate events 3. **Working with Animations:** - Use `AnimatedCardsGameComponent` for card rendering - Position cards in grids using calculated vectors - Cards automatically handle face-up/face-down rendering 4. **UI Management:** - Create buttons with `Button` class - Show/hide buttons based on game state - Use `SpriteBatch` for custom text rendering 5. **Game Flow:** - Use state enums to control phases - Update UI based on current state - Use async delays for timed transitions ### The Magic Trick Pattern This trick demonstrates a key card game concept: **controlled card positioning through mathematical manipulation**. The same pattern appears in: - Card sorting algorithms - Deck cutting tricks - Dealing patterns in games like Bridge ### Next Steps Try modifying the trick: - Use 21 cards in a 7x3 grid (requires 3 selections) - Add a "shuffle" phase between selections - Let the player choose the reveal method - Create different grid layouts This framework makes it easy to experiment with card logic without worrying about rendering or input handling! --- ## Troubleshooting ### Cards Don't Appear - Check that `LoadContent()` was called - Verify card textures exist in Content/Images/Cards/ - Ensure `IsFaceDown = false` is set ### Buttons Don't Respond - Confirm `EnabledGestures` includes `GestureType.Tap` - Check button `Visible` property - Verify `Game.Components.Add(button)` was called ### Wrong Card Revealed - Debug the `RearrangeCards()` logic - Print `TableCards` order after each rearrange - Verify columns are collected correctly (index % 3) ### State Machine Stuck - Add debug output in `Update()` to track state changes - Check that all state transitions are implemented - Verify async tasks complete properly --- ## Complete File Checklist - [ ] `3-Games/MagicTrick/Core/MagicTrickGameState.cs` - [ ] `3-Games/MagicTrick/Core/MagicTrickCardGame.cs` - [ ] `3-Games/MagicTrick/Players/MagicTrickPlayer.cs` - [ ] `3-Games/MagicTrick/Rules/CardSelectionRule.cs` - [ ] `3-Games/MagicTrick/Rules/RevealRule.cs` - [ ] `2-Core/Screens/MagicTrickGameplayScreen.cs` - [ ] Modified `2-Core/Screens/MainMenuScreen.cs` --- ## Conclusion Congratulations! You've built a complete interactive magic trick using the CardsStarterKit framework. You now understand: - How to extend the framework for custom card games - How to use the rule system for game logic - How to manage game state and flow - How to create interactive UI with buttons - How card animations work This foundation prepares you to build more complex card games. The next tutorial covers **Gin Rummy**, which introduces multi-phase gameplay, meld detection, scoring, and NPC opponents. **Happy coding and enjoy amazing your friends with your magic trick!**