ババ抜き、奇数枚の人勝ちやすい説を検証
UTAP advent calender 2日目の記事です。
当初は音声+ディープラーニング系でなにか書こうかなと思ってたんですが、面白いネタが思い浮かばなかったのと、気付いたら投稿日当日になっていて時間がヤバかったので、全く違う内容になりました。(現在12/2 1:30)
この記事の趣旨
ババ抜きというかの有名なトランプのゲームがあります。始めに参加者に均等に配り、一枚ずつ他者から抜き取り同じ札があれば捨て、最後にジョーカーを持っている人が負けという極めてシンプルなゲームです。
しかし、トランプの枚数は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倍ほどにもなっています。
プレイヤーの1位抜け数を、ゲーム数を、プレイヤー人数をとし、各プレイヤーの勝率に対して
として、有意水準5%で検定をします。
プレイヤーの勝率がに従うとすると
とすると、この値は漸近的に自由度のカイ二乗分布に従うので、これを検定統計量として、
が成立すれば、帰無仮説は棄却されます。
試しにの場合でscipy
のchisquare
という関数を使って検定を行うと、
>>> 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
であるので、は棄却されます。同様に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の手札から引きます。 縦軸がプレイヤー、横軸が順位です。
これを見ると、奇数枚プレイヤーの中でも、前の人が偶数枚プレイヤーであるプレイヤーはより有利になっていることが見て取れます(参加人数5人、7人の時に顕著)。前の人が上がると、そのターンはカード引いてもらえなくなるため、自分の手札の偶奇が入れ替わります。そのため、せっかく奇数枚でも前の人に上がられると偶数枚になってしまうのです。つまり奇数枚の場合は、前の人に早く上がられない方が強い = 前の人が偶数枚プレイヤーである奇数枚プレイヤーはより有利、と考えられます。 逆に言えば、前の人が奇数枚プレイヤーである偶数枚プレイヤーは偶数枚プレイヤーの中では比較的有利であることが参加人数: 8人、10人の場合に現れています。
引き始めが早い人有利説
ついでに、引き始めの順番で勝率が変わるのかというのも検証しました。通常ここはじゃんけんなどで決まることが多いため、意図的な調整は厳しいですが、あくまで参考までに。同じく参加人数5人〜10人の場合についてそれぞれ10000ゲーム行っています。青線が1位、橙線がビリになった数です。
最初に引く人は単純に枚数が1枚多くなるので、不利になる傾向があります。最初に引かれる人が一番有利なようです。ただし9人の場合の時のみ、最初に引く人が圧倒的に勝率が高いという結果が得られました。これはおそらく、9人の場合はほとんどの人(8人)が偶数のため、初めに引くと奇数になって有利になるのではないでしょうか。
まとめ・展望
- 手札が奇数枚のプレイヤーは有利であることがわかった
- 友達とババ抜きをやるときは、積極的にカードを配る役に回ろう
- 引き始めの順番を決めるじゃんけんでは、自分の前の人に勝ってもらおう
人数や奇数枚 : 偶数枚の人数比と勝率の関係を時間的にあまり詳しく考察できなかったので、暇な時に考えたいと思います。ババ抜きはクソゲー。