ババ抜き、奇数枚の人勝ちやすい説を検証

UTAP advent calender 2日目の記事です。

qiita.com

当初は音声+ディープラーニング系でなにか書こうかなと思ってたんですが、面白いネタが思い浮かばなかったのと、気付いたら投稿日当日になっていて時間がヤバかったので、全く違う内容になりました。(現在12/2 1:30)

この記事の趣旨

ババ抜きというかの有名なトランプのゲームがあります。始めに参加者に均等に配り、一枚ずつ他者から抜き取り同じ札があれば捨て、最後にジョーカーを持っている人が負けという極めてシンプルなゲームです。

http://www.asahi.com/special/kotoba/archive2015/S2007/upload/2014102500002_1.jpg

しかし、トランプの枚数は52 + 1枚で素数なため、均等に配るとはいえ全ての参加者に同じ枚数だけ配ることは不可能です。つまり最初の枚数で必ず1枚の差が生じます。

この時、奇数枚であれば最後は1枚になるので前の人に引かれれば無条件で上がれますが、偶数枚だと最後ペアが出来ないと上がれないので、奇数枚の方が有利なんじゃないかという気がします。ので実際にシミュレーションして奇数枚の人は本当に有利なのか?有利だとしてどれくらい勝率が変わるのか?というのを検証していきたいというのが今回の趣旨です。

もし奇数枚が本当に有利なのであれば、毎回トランプを配るという役を引き受けて、ランダムに配るフリをしてこっそり自分の手札は必ず奇数枚になるようにすれば勝率が上がることになります。トランプを配るのは普通面倒くさいので、率先して配る人がいてもきっと誰も文句を言わないでしょう。

 

ソースコードとその説明

シミュレーションに用いたコードについて説明します。python3で書かれています。興味ない人は読み飛ばしてください。

まず、トランプのカードのクラスを定義します。

class Card(object):
    suits = ['spade', 'club', 'heart', 'diam']

    def __init__(self, suit, number=1):
        self.suit = suit
        assert suit in self.suits or suit == 'JOKER'
        self.number = number
        assert 1 <= number <= 13 or number is None

    def __repr__(self):
        return '{}_{}'.format(self.suit, self.number) if self.suit != 'JOKER' else 'JOKER'

    def __eq__(self, other):
        return self.suit == other.suit and self.number == other.number

    def __hash__(self):
        return hash(self.number) + hash(self.suit)

__repr____eq__pythonの特殊メソッドで、__repr__print関数によって呼び出した時の出力を指定でき、__eq__インスタンス同士の比較演算子==の挙動を定義できます。 __ne__!=です。通常セットで定義します。

次に、ババ抜きに参加するプレイヤーのクラスを定義します。

class Agent(object):

    def __init__(self, index):
        self.index = index
        self.cards = set()
        self.next_ = None
        self.prev_ = None

    def draw(self, card):
        for c in self.cards:
            if card.number == c.number:
                self.cards.remove(c)
                break
        else:
            self.cards.add(card)

    def drawn(self):
        assert len(self.cards)
        card_ = random.sample(self.cards, 1)
        card = card_[0]
        self.cards.remove(card)
        return card

    def empty(self):
        return len(self.cards) == 0

    def __repr__(self):
        return 'Player_{}'.format(self.index)

カードを引く動作、引かれる動作、などの関数を用意しています。next_, prev_にはカードを引く人と引かれる人のインスタンスが代入されます。便宜上ゲームに参加するプレイヤーには1人ずつ0から順にindexを割り当てます。

次に1ゲームを行うクラスを定義します。

class Game(object):

    __BEGIN__ = 0
    __PLAYING__ = 1
    __END__ = 2

    def __init__(self, n_people, verbose=False, serve_randomly=True, begin_randomly=True):
        """
        Parameters
        ----------
        n_people: 参加人数
        verbose
        serve_randomly : 配り始める位置をランダムにするか0からにするか
        begin_randomly : ターンを始める位置をランダムにするか0からにするか
        """
        self.n_people = n_people
        self.agents = [Agent(index=i) for i in range(n_people)]
        for i in range(n_people):
            if i == 0:
                self.agents[i].prev_ = self.agents[n_people - 1]
            else:
                self.agents[i].prev_ = self.agents[i - 1]

            if i == n_people - 1:
                self.agents[i].next_ = self.agents[0]
            else:
                self.agents[i].next_ = self.agents[i + 1]

        self.results = []
        self.now_agent = None
        self.status = self.__BEGIN__
        self.verbose = verbose
        self.serve_randomly = serve_randomly
        self.begin_randomly = begin_randomly

    def serve_cards(self):
        cards = []
        for suit in Card.suits:
            for number in range(1, 14):
                cards.append(Card(suit, number))
        cards.append(Card('JOKER'))

        random.shuffle(cards)

        if self.serve_randomly:
            now_agent = random.sample(self.agents, 1)[0]
        else:
            now_agent = self.agents[0]

        for card in cards:
            now_agent.draw(card)
            now_agent = now_agent.next_

        for i in range(self.n_people):
            if now_agent.empty():
                self.win_out(now_agent)
            now_agent = now_agent.next_

    def transaction(self):
        if len(self.agents) == 1:
            self.results.append(self.agents[0].index)
            self.status = self.__END__
            return

        card = self.now_agent.next_.drawn()
        if self.now_agent.next_.empty():
            self.win_out(self.now_agent.next_)

        self.now_agent.draw(card)
        if self.now_agent.empty():
            self.win_out(self.now_agent)

        self.now_agent = self.now_agent.next_

        self.status = self.__PLAYING__
        return

    def win_out(self, agent):
        if self.verbose:
            print('{} win!'.format(agent))
        agent.prev_.next_ = agent.next_
        agent.next_.prev_ = agent.prev_
        self.results.append(agent.index)
        self.agents.remove(agent)

    def play(self):
        self.serve_cards()

        # start!
        if self.begin_randomly:
            self.now_agent = random.sample(self.agents, 1)[0]
        else:
            self.now_agent = self.agents[0]

        self.status = self.__PLAYING__

        turn = 1
        while self.status != self.__END__:
            self.transaction()
            if self.verbose:
                print('turn:{} agent:{}'.format(turn, self.now_agent))
                self.dump_game_status()

            turn += 1

        self.dump_results()

    def dump_results(self):
        print('-'.join(map(str, self.results)))

    def dump_game_status(self):
        print('---------')
        for agent in self.agents:
            print('{}: {}'.format(agent, agent.cards))

まあまあ長い。配り方は1枚ずつ時計回りで配っていくと仮定しています。 ここで、配り始めの人をplayer(index=0)に固定するかランダムにするか、カードを引き合うフェイズに入った時に最初にカードを引き始める人をplayer(index=0)に固定するかをパラメータとして与えることができます。

7人で配り始めを固定してゲームを行う例です。

game = Game(
    n_people=7,
    serve_randomly=False,
    begin_randomly=True
)
game.play()
print(game.result)

オブジェクト指向言語はこういうのが直感的に書けるので好きです。

結果

奇数枚有利説

参加人数を5人〜10人の場合についてそれぞれ10000ゲーム行い、奇数枚の人と偶数枚の人の1位抜け率とビリ率を算出しました。

参加人数 奇数枚 偶数枚 奇数枚の1位率平均 偶数枚の1位率平均 奇数枚のビリ率平均 偶数枚のビリ率平均
5人 3人 2人 0.2458 0.1313 0.1753 0.2369
6人 5人 1人 0.1833 0.0835 0.1579 0.2101
7人 3人 4人 0.2199 0.085 0.1029 0.1728
8人 5人 3人 0.157 0.0716 0.1045 0.1591
9人 1人 8人 0.2801 0.0899 0.0433 0.1195
10人 7人 3人 0.1273 0.0361 0.0832 0.1391

全ての人数において、1位率、ビリ率共に奇数枚が有利な結果になっているように見えます。特に奇数枚が1人である参加人数: 9人の場合においては、偶数枚の人に対して1位率が3倍ほどにもなっています。

プレイヤー iの1位抜け数を X_i、ゲーム数を k = 10000、プレイヤー人数を nとし、各プレイヤーの勝率 p_iに対して

 \displaystyle
H_0 : p_1 = p_2 = ... = \frac{1}{n}

 \displaystyle
H_1 :  not \,\, p_1 = p_2 = ... = \frac{1}{n}

として、有意水準5%で検定をします。

プレイヤーの勝率が H_0に従うとすると

f:id:sumsum88:20171202131004p:plain:w150

とすると、この値は漸近的に自由度 {n-1}カイ二乗分布に従うので、これを検定統計量として、

f:id:sumsum88:20171202130334p:plain:w130

が成立すれば、帰無仮説 H_0は棄却されます。

試しに n = 5の場合でscipychisquareという関数を使って検定を行うと、

>>> from scipy.stats import chisquare

>>> x = [2546, 2428, 2400, 1285, 1341]

>>> k = 10000

>>> n = 5

>>> chisquare(f_obs=x, f_exp=[k/ n ] * n)
Power_divergenceResult(statistic=793.40300000000002, pvalue=2.0619817457860561e-170)

pvalue < 0.05であるので、 H_0は棄却されます。同様にn = 6 ~ 10についても、棄却されます。茶番

n pvalue
5 2.06e-170
6 3.60e-108
7 0.0
8 1.41e-233
9 0.0
10 0.0

少し細かくプレイヤーの順位分布を見てみます。下の図は10000回中の各プレイヤーの各順位の数をカラーマップ化したものです。明るいほど数が大きいことを表します。カードはプレイヤー1、プレイヤー2... の順に1枚ずつ配っていくとしています。カードを引くときはプレイヤーnがプレイヤーn+1の手札から引きます。 縦軸がプレイヤー、横軸が順位です。

f:id:sumsum88:20171202110333p:plain

これを見ると、奇数枚プレイヤーの中でも、前の人が偶数枚プレイヤーであるプレイヤーはより有利になっていることが見て取れます(参加人数5人、7人の時に顕著)。前の人が上がると、そのターンはカード引いてもらえなくなるため、自分の手札の偶奇が入れ替わります。そのため、せっかく奇数枚でも前の人に上がられると偶数枚になってしまうのです。つまり奇数枚の場合は、前の人に早く上がられない方が強い = 前の人が偶数枚プレイヤーである奇数枚プレイヤーはより有利、と考えられます。 逆に言えば、前の人が奇数枚プレイヤーである偶数枚プレイヤーは偶数枚プレイヤーの中では比較的有利であることが参加人数: 8人、10人の場合に現れています。

引き始めが早い人有利説

  ついでに、引き始めの順番で勝率が変わるのかというのも検証しました。通常ここはじゃんけんなどで決まることが多いため、意図的な調整は厳しいですが、あくまで参考までに。同じく参加人数5人〜10人の場合についてそれぞれ10000ゲーム行っています。青線が1位、橙線がビリになった数です。   f:id:sumsum88:20171202120836p:plain

最初に引く人は単純に枚数が1枚多くなるので、不利になる傾向があります。最初に引かれる人が一番有利なようです。ただし9人の場合の時のみ、最初に引く人が圧倒的に勝率が高いという結果が得られました。これはおそらく、9人の場合はほとんどの人(8人)が偶数のため、初めに引くと奇数になって有利になるのではないでしょうか。

まとめ・展望

  • 手札が奇数枚のプレイヤーは有利であることがわかった
  • 友達とババ抜きをやるときは、積極的にカードを配る役に回ろう
  • 引き始めの順番を決めるじゃんけんでは、自分の前の人に勝ってもらおう

  人数や奇数枚 : 偶数枚の人数比と勝率の関係を時間的にあまり詳しく考察できなかったので、暇な時に考えたいと思います。ババ抜きはクソゲー

ソースコードgithubに上げてあります。

github.com