# 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!**