1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

カードマジック(カード当て)の自作自演感がすごい

Last updated at Posted at 2024-08-30

簡単なカードマジックを何故か実装してみた.
【客がカードを1枚覚えて… → 何やかんやあって… → それを当てる】っていうやつ.

……っていう,ただの初心者の日記です.

実装

※話を簡単にするため,全体的に,引数がイレギュラーな場合(範囲外とか)を考えない実装になっている.

カード

何はともあれカードの表現が必要.
至極単純に 52枚のカードを 0~51 の値で表すこととした.

コード
//[CardDef.h]
#pragma once
#include <cstdint>

namespace Card
{
    //4種類のスート
    enum class Suit { S=0, H=1, D=2, C=3 };

    //※カードID仕様:52枚のカードを { 0, 1, ..., 51 } の 52個のID値で表す.
    //(ジョーカーは無いものとする)
    using CardID = uint8_t;

    //カードのスートを得る(カードID仕様外の引数値を与えた場合は動作不定)
    inline Suit SuitOf( CardID cid )
    {
        static constexpr Suit Suits[4] = { Suit::S, Suit::H, Suit::D, Suit::C };
        return Suits[ cid/13 ];
    }

    //カードの値(1~13)を得る(カードID仕様外の引数値を与えた場合は動作不定)
    inline int ValueOf( CardID cid )
    {   return (cid % 13) + 1;  }

#if 0   //未使用
    //スートと値からカード値を得る(引数 v が 1~13 でない場合の動作は不定)
    inline CardID ID( Suit s, int val )
    {   return (CardID)( (int)s * 13 + (val - 1) ); }
#endif
}

カードの束

その時々で「デック」とか「パケット」とか「パイル」だとか色々と呼び名があるらしいが(よくわからん),
ここでは Deck という名前のクラスとした.

  • std::list<CardID> というデータでカード群を保持し,
  • 必要な操作をメソッドとして持つ

というだけの物である.

MEMO:
とりあえず「ランダムにシャッフルする」っていう処理を書こうとしたら std::list だと std::shuffle ができなくて困った.
仕方ないので「一回 vector にコピーしてそこでシャッフルして結果を list にコピー」という形で逃げた.
コレもっとマシな方法は無いのだろうか?

コード
//[Deck.h]
#pragma once
#include "CardDef.h"
#include <list>
#include <random>
#include <algorithm>
#include <vector>

namespace Card
{
    //デック(パケット,パイル):カードの束.
    //※各メソッドに関して,カード位置や枚数のような引数がイレギュラーな場合の動作は不定
    class Deck
    {
    public:
        const bool empty() const {  return m_Cards.empty(); }
        const size_t size() const { return m_Cards.size();  }

        //指定位置のカードを見る
        CardID operator[]( size_t pos_indx ) const {    return *std::next(m_Cards.begin(),pos_indx);    }

        //トップにカードを追加
        void AddToTop( CardID Card ){   m_Cards.push_front(Card);   }
        //D全体をトップに乗せる.
        void AddToTop( Deck D ){    m_Cards.splice( m_Cards.begin(), D.m_Cards );   }
        //ボトムにカードを追加
        void AddToBottom( CardID Card ){    m_Cards.push_back(Card);    }
        //D全体をボトム側に追加する.
        void AddToBottom( Deck D ){ m_Cards.splice( m_Cards.end(), D.m_Cards ); }
        //指定位置にカードを挿入
        void Insert( CardID Card, size_t pos_indx )
        {   m_Cards.insert( std::next(m_Cards.begin(),pos_indx), Card );    }

        //指定位置のカードを抜き出す
        CardID PullOut( size_t pos_indx )
        {
            auto iTgt = std::next( m_Cards.begin(), pos_indx );
            CardID TgtCard = *iTgt;
            m_Cards.erase( iTgt );
            return TgtCard;
        }

        //トップから n 枚の束を取る
        inline Deck CutFromTop( size_t n )
        {
            Deck PickedPacket;
            PickedPacket.m_Cards.splice( PickedPacket.m_Cards.begin(), m_Cards, m_Cards.begin(), std::next(m_Cards.begin(),n) );
            return PickedPacket;
        }

        //引数の乱数エンジンを用いて std::shuffle でシャッフルする
        template< class RndEng >
        void Shuffle( RndEng &Eng )
        {
            std::vector<CardID> Tmp( m_Cards.begin(), m_Cards.end() );
            std::shuffle( Tmp.begin(), Tmp.end(), Eng );
            std::copy( Tmp.begin(), Tmp.end(), m_Cards.begin() );
        }

        //ヒンドゥーシャッフル
        //* nMaxMoveCardAtOnce : 一度に右手から左手に移動させるカードの最大枚数.1未満を指定した場合は1として扱われる.
        template< class RndEng >
        inline void HinduShuffle( RndEng &Eng, size_t nMaxMoveCardAtOnce=3 )
        {
            std::uniform_int_distribution<size_t> RND( 1, std::max(size_t(1),nMaxMoveCardAtOnce) );
            Deck LeftHandPacket;
            while( !empty() )
            {
                size_t n = (std::min)( RND(Eng), size() );
                LeftHandPacket.AddToTop( std::move(CutFromTop(n)) );
            }
            m_Cards = std::move(LeftHandPacket.m_Cards);
        }

    private:
        std::list< CardID > m_Cards;    //並びは{先頭がトップカード,末尾がボトムカード}とする.
    };
}

マジックの実装

上記部品を用いて,マジックの実装を行った.

マジックの内容(しょぼい)は以下のようなもの:

  • (ジョーカー抜きの52枚のデックを使用する)
  • 演者がデックをシャッフルした後,客に上から15枚前後を取り上げてもらい,
    それをシャッフルしてからテーブルの上に置いてもらう.
  • 再度,同じくらいの枚数を取り上げて→これもシャッフルして→テーブル上に置いてもらう.
  • (演者の手元に残ったカードはこれ以降使わないので脇に置いておく)
  • 客はテーブル上の2つのカードの束のうちの好きな方の束から任意の1枚を抜き出して覚え,
    その覚えたカードをもう一方のカードの束の中に入れ,どのカードなのかがわからないようにその束を激しくシャッフルしてから演者に手渡す.
  • 演者は渡されたパケットを検めて1枚のカードを抜き出す→客の覚えたカードである.

これの手続きをひたすらコード化した.

コード
//[XXX.cpp]
#include <iostream>
#include "Deck.h"
using namespace Card;
using UIDist = std::uniform_int_distribution<size_t>;

namespace SimpleMagic
{
    //手品のタネの根幹
    bool Is1stGroup( CardID cid )
    {
        constexpr int Vals[] = { 1,3,8,9,10,13 };
        bool IsOneOfVals = ( std::find( std::begin(Vals), std::end(Vals), ValueOf(cid) ) != std::end(Vals) );
        auto Suit = SuitOf(cid);
        return (  ( Suit==Suit::S || Suit==Suit::H )   ?   IsOneOfVals   :   !IsOneOfVals  );
    }

    //初期状態(:手品のタネがセットされている状態)のデックを生成
    template< class RndEng >
    Deck TakeOutDeckAlreadySetUp( RndEng &Eng )
    {
        Deck Group[2];
        for( CardID cid=0; cid<52; ++cid )
        {   Group[ Is1stGroup(cid) ? 0 : 1 ].AddToBottom( cid );    }

        Group[0].Shuffle( Eng );
        Group[1].Shuffle( Eng );
        Group[0].AddToTop( std::move(Group[1]) );
        return Group[0];
    }

    //作業関数.std::cout にカード名称を出力
    void ShowCardName( CardID cid )
    {   std::cout << "SHDC"[(int)SuitOf(cid)] << ValueOf(cid);  }

    //PerformTheMagicOnce() 内で用いる処理.
    //引数パケットから,客のカードを見つけて抜き出す.
    CardID PullOutAnswerFrom( Deck &FindTgtPacket )
    {
        const size_t n = FindTgtPacket.size();
        std::vector<size_t> X[2];
        for( size_t i=0; i<n; ++i )
        {
            CardID cid = FindTgtPacket[i];
            X[ Is1stGroup(cid) ? 0 : 1 ].push_back( i );
        }

        size_t AnsIndex = n/2;  //※n/2 : 何か失敗して分からなくなってしまった場合に適当なカードを選ぶ
        {
            if( X[0].size()>=2 && X[1].size()==1 )
            {   AnsIndex = X[1].front();    }
            else if( X[0].size()==1 && X[1].size()>=2 )
            {   AnsIndex = X[0].front();    }
        }
        return FindTgtPacket.PullOut( AnsIndex );
    }

    //マジックを1回実施する.
    //引数のデックとしては TakeOutDeckAlreadySetUp() により得られた状態のものが期待されるが,
    //数回程度であれば,この PerformTheMagicOnce() 実施後の状態のデックから開始しても大丈夫なハズ. 
    void PerformTheMagicOnce( Deck &TheDeck, std::mt19937 &mt )
    {
        //客の目の前でデックをシャッフルする.
        //  ヒンドゥーシャッフルかオーバーハンドシャッフルを行う.
        //  (実装的にはどっちも一緒になると思う)
        TheDeck.HinduShuffle( mt );

        Deck Packet[2];
        {
            //デックのトップから15枚程度を取り上げてもらう.
            UIDist nCardDecision( 15-3, 15+3 );
            Packet[0] = TheDeck.CutFromTop( nCardDecision(mt) );
            //残りのデックを再度ヒンドゥーシャッフルしながら,
            //「今取り上げたカードの束をこのようにシャッフルせよ」と指示する.
            TheDeck.HinduShuffle( mt );
            //言われた通りに客もシャッフルする.シャッフルし終えたらテーブルに置いてもらう.
            Packet[0].HinduShuffle( mt );   //(※客のシャッフル手法は何でも良いのだが)

            //またさっきと同じくらいの枚数を取り上げてもらう.
            //同様にシャッフルしてからテーブルに置いてもらう.
            Packet[1] = TheDeck.CutFromTop( nCardDecision(mt) );
            Packet[1].HinduShuffle( mt );   //(※客のシャッフル手法は何でも良いのだが)

            //(手元に残っているカードは使わないので脇に置いておく)
        }

        //テーブル上にできた2つのパケットのうち,好きな方から1枚を抜き出して覚えてもらう.
        //で,そのカードを他方のパケットの中に混ぜ込んでもらう.
        auto *pPullTgtPacket = &(Packet[0]);    //一枚引く側のパケット
        auto *pFindTgtPacket = &(Packet[1]);    //混ぜ込む側のパケット
        {
            //(どっちのパケットからカードを抜くのかは客の任意.ここでは乱数で決めている)
            if( mt() & 0x01 ){  std::swap( pPullTgtPacket, pFindTgtPacket );    }
            //(どのカードを抜くのかも乱数で)
            UIDist PullPosDecision{ 0, pPullTgtPacket->size()-1 };
            CardID UserCard = pPullTgtPacket->PullOut( PullPosDecision(mt) );
            std::cout << "( The card you chose is [";
            ShowCardName( UserCard );
            std::cout << "] )\n";

            //他方のパケットに混ぜ込む(適当な位置に挿入し,適当にシャッフルしてもらう)
            UIDist InsertPosDecision{ 0, pFindTgtPacket->size()-1 };
            pFindTgtPacket->Insert( UserCard, InsertPosDecision(mt) );
            pFindTgtPacket->Shuffle( mt );  //激しくシャッフル
        }
    
        //客が覚えたカードが混ぜ込まれたパケットを受け取り,
        //その中から客のカードを取り出して見せる.当たってるかな?
        CardID Ans = PullOutAnswerFrom( *pFindTgtPacket );
        std::cout << "Maybe, your card is ... [";
        ShowCardName( Ans );
        std::cout << "]\n";
    
        //---
        //(後処理)とっ散らかったカードを全部重ねて52枚のデックに戻す.
        //このとき,タネのセットが(100%ではないにしろ)復活するように重ねる.
        
        //* 脇に置いておいたデックに客のカードを何気なく(適切な場所に)加えて…
        if( pPullTgtPacket == &Packet[0] )
        {   TheDeck.AddToBottom( Ans ); }
        else
        {   TheDeck.AddToTop( Ans );    }
        //* さらに2つのパケットもデックの適切な位置に戻す
        TheDeck.AddToBottom( std::move(Packet[0]) );
        TheDeck.AddToTop( std::move(Packet[1]) );
    }
}

//-------------------------------------
//main
int main()
{
    //乱数用
    std::mt19937 mt( std::random_device{}() );

    //あらかじめタネがセットされた状態のデックを取り出してマジックを始める
    auto TheDeck = SimpleMagic::TakeOutDeckAlreadySetUp( mt );

    //※何度か繰り返し実施してみる.
    for( int i=0; i<20; ++i )   //さすがに20回とかやるとさすがに途中で無理が出たりすると思う
    {
        std::cout << "(" << i << ")----------\n";
        SimpleMagic::PerformTheMagicOnce( TheDeck, mt );
    }
    return 0;
}

動作結果

実行すると以下のように結果が表示されます.(以下は1回分の出力)

( The card you chose is [C13] )
Maybe, your card is ... [C13]

客が覚えたカードがクラブの13で → それに対して「クラブの13ですよね?」と.
よっしゃーー! 当たった!!

うん,何だかものすごくインチキ感というか自作自演感みたいなのが……何故だろう? 不思議.

追記:
うまいこと20回やり切る場合もそこそこあるみたい.
演者がヒンドゥーシャッフルするところで第二引数の値を大きくしてやる(:より「シャッフルへたくそ」にする)と,早く失敗するようになる.

まぁ実際に人間がやる場合だと「中間付近では細かく,それ以外は雑に」とかいい感じに調整すると思われるので,そういうのを実装に反映させてやればマシになる(連続成功回数が伸びる)のかもしれない.


実演に関するどうでもいい話

(ジョーカー抜きの52枚のデックを使用する)

実際には,ジョーカーもデックの適当な位置に入れておき,
始める前に「ジョーカーは使わないから取り除く」と言って目の前でカードを表向きにスプレッドしてジョーカーを除去してみせる,ということをすれば
「変な細工とかしてませんよ/カードは十分に混ざってますよ」感みたいなのを出せるのかな?
それとも演者がカードの表面を見る機会があるというのはかえって「怪しい…」って感じになるのかな?
どうなんだろう?

客に上から15枚前後を取り上げてもらい…

最初は「デックにジョーカー2枚を差し込んでもらう」とかいう形を考えてたんだけど,それだとデックの 上側/下側 を使う(真ん中を使わない)ことが露骨な光景が生じるので,記事内に記した手順に改めた.
まぁどっちでもあまり変わりはない気がする.

あと,(内容が子供だましなだけに)子供相手を想定しているのだけど,実際何度かやってみた感じ,子供って予想外に「デックが52枚」だということ自体を知らない(とか即答できない)率が高いので注意が必要.(最近の子供はトランプとか触る経験が少ないのかな?)
なので「15枚程度がどうの~」とか指示を出す前に,最初にデックを取り出したときに「全部で何枚あるでしょう? 知ってる?」みたいな話を入れた方がいいっぽい.

ところでこの「15枚」程度というのは,このプログラムがある程度の回数成功できるように調整したような値であり,かなり少ない(防御的な?)指定である.
実際には「15枚から20枚の間くらい」とか「20枚程度」とか言っても大丈夫なハズ.
(少なすぎると「この中から当てるよ」っていうときの分母として微妙になる気がするし)

1
0
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?