Click here to Skip to main content
15,867,594 members
Articles / Desktop Programming / Windows Forms

21st Card Magic in C#.NET

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
10 Nov 2016CPOL8 min read 18.9K   546   16   3
Coding a simple magic with cards!

Introduction

21st card magic is a famous card trick that is performed using 21 cards. The magician shuffles cards in 3 decks and re-shuffles the decks according to the deck that contains the preferred card. This is a fun project that depicts the trick in a computer program.

Image 1

Background - The Card Trick

I shall not go into details of the trick as there are numerous tutorials on the net about how to perform the magic. To summarize, the magician first shuffles the 21 cards in 3 decks. Cards remain upside down, and the magician picks the last card, then places it on the 1st deck, then picks the next card, puts in the 2nd deck, then picks the next card and puts in the 3rd deck. The next card will be placed on the 1st deck, and so on.

After shuffling, the magician picks the decks one by one and asks the observer if the chosen card is in the deck. Whichever deck has the card, it goes in the middle of the other two decks.

Then the magician re-shuffles the cards in the same way.

After 3 shuffles, the magician starts popping the cards from the reverse side and the chosen card is always at the 11th position. S/he shows the card to the observer and baffles him/her.

There is a mathematical explanation of how the card always places itself at the 11th position. A quick and careful observation is it is the middle card of the final combined deck – 10 cards at the front, 10 at the back; the chosen card is automatically put in the middle.

Keywords Used in this Article

  • Deck: A group of cards
  • Card-front: The side of a card that has the actual value
  • Card-back: The backside of the card that doesn’t show anything; similar graffiti is painted across all the cards just as a background image
  • Magician: The performer of the trick
  • Observer: The person who chooses a card

Using the Code

Some data structures are used for this project namely – List, KeyValuePair, HashTable and Stack. The magic incurs the following steps:

  1. Loading the cards from resource file.
  2. Load the cards in the pictureboxes randomly.
  3. Allow the user to choose the card; s/he affirms this by clicking on the button that contains the card.
  4. After the user chooses the row, place that row in the middle of the other two rows.
  5. Allow the user to choose 3 times. After the 3rd reshuffling, start popping the card and display the 11th card from reverse (or from the front – it doesn’t matter at this point as the chosen card is always in the middle).

1) Loading the Cards

In design time, pictureboxes are used and images are selected using the in-built IDE assistance.

Image 2

By doing this, images are loaded in the pictureboxes and the IDE automatically keeps the images in the resource file.

Image 3

Two objects are used to store the cards – one is stack type (‘ShuffledCards’), the other one (‘Cards’) is list type. Both stores cards as KeyValuePair objects. A KeyValuePair object stores an object along with its name. E.g.: KeyValuePair for Diamond Ace: {“Diamond Ace”, Image of diamond ace}.

Image 4

The idea is to randomly load images in the pictureboxes. First, all the resources are loaded in a ResourceSet object. Then all the resources are restored in the ‘Cards’ list.

C#
ResourceSet resourceSet = 
   Properties.Resources.ResourceManager.GetResourceSet(CultureInfo.CurrentUICulture, true, true);    

foreach (DictionaryEntry entry in resourceSet)
    //Cards.Add(entry.Value);
    Cards.Add(new KeyValuePair<string, object>(entry.Key.ToString(), entry.Value)); 

2) Loading Cards in Picture Boxes Randomly

After loading the image resources, we opt for randomly placing the images in the pictureboxes. The randomizer is initialized. A random index from (1 to 21 inclusive) is generated. Please note that if you specify the range (1 to 22), it will generate a random number between 1 and 21 inclusive. Actually, we need an index between (0 to 20), for some reason 0 was never generated, so I resorted to this solution of generating between (1 and 21 inclusive) and then deduct 1 to obtain the actual index.

The pictureboxes are named as ‘picturebox0’, ‘picturebox1’, ‘picturebox3’….. and so on up to ‘picturebox20’.

Image 5

C#
Random Rnd = new Random(DateTime.Now.Millisecond);
int Idx;
Hashtable HashTbl = new Hashtable();
...................
...................
for (int i = 0; i < 21; i++)
    {
        Idx = Rnd.Next(1, 22);
        while (ThisImageWasAlreadyTaken(--Idx, HashTbl))
            Idx = Rnd.Next(1, 22);

        HashTbl.Add(Idx, Idx);
...................
...................

The hashtable is used just to check if the index was already taken. The method ThisImageWasAlreadyTaken() simply returns true/false according to the index availability in the hash table.

C#
private bool ThisImageWasAlreadyTaken(int Idx, Hashtable HashTbl)
{
    return HashTbl.ContainsKey(Idx);
}  

The pictureboxes are placed like below in the design form. Now it is time to find the pictureboxes by their name and place the random image there. This is done in the following code. Please see that Controls.Find() method returns the control according to the string supplied. The second argument instructs whether it should search for child controls as well. This is set to ‘false’ as we know that there are no child controls.

C#
Control[] PicBox;
PicBox = Controls.Find("PictureBox" + i, false);
(PicBox[0] as PictureBox).Image = (Image)Cards[Idx].Value;
ShuffledCards.Push(Cards[Idx]);

The last line of the above code is a stack operation of pushing. Please note that C# push-pop operates in the standard way, however the way it stores the values requires a little attention.

For example, for the first loading as shown in the above image, the ShuffledCards stack will contain:

[0]: {[Spade Jack, System.Drawing.Bitmap]}
[1]: {[Diamond 10, System.Drawing.Bitmap]}
[2]: {[Spade Ace, System.Drawing.Bitmap]}
[3]: {[Club Queen, System.Drawing.Bitmap]}
[4]: {[Heart 10, System.Drawing.Bitmap]}
[5]: {[Heart Queen, System.Drawing.Bitmap]}
[6]: {[Spade Queen, System.Drawing.Bitmap]}
[7]: {[Diamond Queen, System.Drawing.Bitmap]}
[8]: {[Club 10, System.Drawing.Bitmap]}
[9]: {[Diamond Jack, System.Drawing.Bitmap]}
[10]: {[Diamond Ace, System.Drawing.Bitmap]}
[11]: {[Spade King, System.Drawing.Bitmap]}
[12]: {[Heart Jack, System.Drawing.Bitmap]}
[13]: {[Heart King, System.Drawing.Bitmap]}
[14]: {[Diamond King, System.Drawing.Bitmap]}
[15]: {[Club Ace, System.Drawing.Bitmap]}
[16]: {[Club Jack, System.Drawing.Bitmap]}
[17]: {[Heart Ace, System.Drawing.Bitmap]}
[18]: {[Club King, System.Drawing.Bitmap]}
[19]: {[Spade 2, System.Drawing.Bitmap]}
[20]: {[Spade 10, System.Drawing.Bitmap]}    

That is, it keeps stacking over (if we think of a vertical stack). The first one we pushed was ‘Spade 10’, it was pushed in the first index, however as we keep pushing other stuff, it eventually moves to the last index. Anyway, index is not what we are dealing with – it is the push/pop that matters in stacks. Just wanted to focus some lights about how stacking works in C#. There is no index operations on stack (although a LINQ extension method ElementAt() can be used, which is not a soul method of stack

3) User Interaction

After the observer chooses a card and clicks on the row button, then the current shuffled cards are copied in a temporary list called ‘OldShuffledCards’. Then, as the card-trick says, the row containing the card will go in the middle of the other two rows. This is done in the following line (if the chosen card is in the first row).

Now let’s see how this is accomplished. If we look at the above distribution, let’s say the observer chooses ‘Heart Ace’ (which is the 2nd of the top row from left), thereby it is in the first row. So technically, all the cards in this row will move in between the other two rows. This is accomplished in 3 consecutive loops.

The first loop pushes all the cards of the second row. If we look carefully, on the first iteration, the OldShuffledCards[1] is pushed which is ‘Diamond 10’ according to the above distribution. Similarly, it pushes all the cards in this row.

The next loop pushes all the cards of the first row. If we look carefully, on the first iteration, the OldShuffledCards[2] is pushed which is ‘Spade Ace’ according to the above distribution. Similarly, it pushes all the cards in this row.

The next loop pushes all the cards of the third row. If we look carefully, on the first iteration, the OldShuffledCards[0] is pushed which is ‘Spade Jack' according to the above distribution. Similarly, it pushes all the cards in this row.

Thus the row in question is placed in between the other two. As expected, the first and third loops can be interchanged – what matters is keeping the row in question in the middle.

C#
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[1 + 3 * i]);
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[2 + 3 * i]);
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[3 * i]);  

In the same way, if the chosen card lies in the 2nd row, then the re-distribution occurs as below:

C#
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[3 * i]);
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[1 + 3 * i]);
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[2 + 3 * i]);  

In the same way, if the chosen card lies in the 3rd row, then the re-distribution occurs as below:

C#
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[2 + 3 * i]);
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[3 * i]);
for (int i = 0; i < 7; i++)
    ShuffledCards.Push(OldShuffledCards[1 + 3 * i]);   

After the re-distribution, we need to re-shuffle the cards on the 3 decks.

For re-distribution, a clone stack is first created called ‘TempCards’. This stack is a clone of our global ShuffledCards. After that, the ShuffledCards is cleared out to hold nothing. Then, all pictureboxes (from 0th to the 20th) are populated by popping the last card from the TempCards stack. At the same time, the popped card is pushed in our global ShuffledCards which will be required in further shuffles. This is done in the following code:

C#
Stack<KeyValuePair<string, object>> TempCards = 
new Stack<KeyValuePair<string, 
object>>(new Stack<KeyValuePair<string, object>>(ShuffledCards));
ShuffledCards.Clear();

Control[] PicBox;

for (int i = 0; i < 3; i++)
    for (int j = 0; j < 7; j++)
    {
        PicBox = Controls.Find("PictureBox" + (j + (i * 7)), false);
        KeyValuePair<string, object> KVP = TempCards.Pop();
        ShuffledCards.Push(KVP);
        if (ShuffleCount < 2)
        {
            Thread.Sleep(20);
            (PicBox[0] as PictureBox).Image = (Image)KVP.Value;
            Application.DoEvents();
        }
    }

This is done in the first two shuffles. A little delay is deliberately offered (Thread.Sleep(20);) so that the re-shuffling of cards can be well-observed on the display.

What about the last re-shuffling? This is also performed, but in a sly manner. If we omit this part and remove the IF condition above, then the observer will always be able to see that the chosen card always places itself at the 10th position from either direction. So to baffle his/her understanding, the last re-shuffle is done on the ‘Cards’ stack which originally had been holding the cards so far from the resource file. This actually loads the images from our old pal Cards, rather than the ShuffledCards (which is holding the chosen card at the 11th position by now). Please note that the last re-shuffling was also done in the above code; it is just the image loading where we had been a little cunning.

C#
if (ShuffleCount == 2)
{
    Hashtable HashTbl = new Hashtable();
    int Idx;
    Random Rnd = new Random(DateTime.Now.Millisecond);
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 7; j++)
        {
            PicBox = Controls.Find("PictureBox" + (j + (i * 7)), false);
            Idx = Rnd.Next(1, 22);
            while (ThisImageWasAlreadyTaken(--Idx, HashTbl))
                Idx = Rnd.Next(1, 22);

            HashTbl.Add(Idx, Idx);
            (PicBox[0] as PictureBox).Image = (Image)Cards[Idx].Value;
            Thread.Sleep(20);
            Application.DoEvents();
        }
}

Finally, if the shuffle count reaches 3, then the 11th card is just displayed.

C#
private void DisplayCard()
{
    Control[] PicBox = Controls.Find("ChosenCardpictureBox", false);
    for (int i = 0; i < 10; i++)
        ShuffledCards.Pop();

    KeyValuePair<string, object> KVP = ShuffledCards.Pop();
    (PicBox[0] as PictureBox).Image = (Image)KVP.Value;

    PlayAgainButton.Visible = true;
    StatusLabel.Text = "Look I found your card!......  Magic!!";
    Row1Button.Enabled = Row2Button.Enabled = Row3Button.Enabled = false;            
}

Environment

The project was accomplished in Visual Studio 2015 IDE, with target .NET Framework 4.5.

Future Works

Animation effects might be applied where a card moves from its position to the combined deck, then again distributed in separate decks. The decks in their own premises can be randomised to expose a more baffling effect to the observer (i.e., in each row, the 7 cards might be randomly placed, however the original track must be maintained in a separate array).

Alternative Data Structures

The Key-value pair is not a must component, a simple list can be used because the card-names were not used. This was used to demonstrate the project more effectively. It is the images that are used which can be put in a list and utilized thereby.

Instead of Stack, a simple list might be used.

References

History

  • 11th November, 2016: First release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Bangladesh Bangladesh
A software developer mainly in .NET technologies and SQL Server. Love to code and learn.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Farhad Reza11-Nov-16 3:39
Farhad Reza11-Nov-16 3:39 
GeneralRe: My vote of 5 Pin
Mehedi Shams11-Nov-16 16:11
Mehedi Shams11-Nov-16 16:11 
GeneralRe: My vote of 5 Pin
Farhad Reza12-Nov-16 5:57
Farhad Reza12-Nov-16 5:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.