ホールデムポーカのプリフロップ戦略と可視化


はじめに

プリフロップでは、はじめに配られる 2枚のカード(ホールカード) のみを頼りに、ゲームに参加するのかを判断しなければならない。

一般的な戦略としては、「この2枚が手元にある場合の勝利確率」という統計データを頭に叩き込む。この統計データは、以下に示すようなマトリックスで表現される。

引用元: https://www.cardschat.com/poker-starting-hands.php

対角線上には、ポケットペアでの勝率が並び、右上にはスーテッド(2枚のカードの種類がそろっている場合)の組み合わせでの勝率が並ぶ。左下にはオフスートの場合の勝利確率が並ぶ。

確率は全部足して 100% になっていない。これは条件付き確率だからだ。つまり、「AAが来た場合に 85% で勝てる」のと、「KKが来た場合に 83% で勝てる」の 85 + 83 を計算することに意味はない。

ちょっと待った!

ポーカーやったことがある人であればすぐに違和感に気づく。「AAで 85%も勝てるなんて直観と反する。もっと勝率は低いはずだ。」確かにこれは正しい直感で、前述の表は、「ヘッズアップ(1 対 1)でのハンドの勝利確率」なのだ。人数が増えればもっと勝率は低くなっていってしまう。

とはいえ、ホールカードとしてどの組み合わせが望ましいかという点では、上述の表は有益であることは間違いがない。

どのようにこの表を作るのか?

基本的に全ての組み合わせを考えて、勝利する確率を求めればよい。ヘッズアップの場合、手元の 2枚を除いた 50枚から、ボードにでる 5枚と、対戦相手にわたる 2枚を選べばいいので、 C(50, 5) * C(45, 2) = 2,097,572,400通りある。ちょっと難しすぎんよ。ということで、モンテカルロなシミュレーションで表を埋める。

シミュレーションでやるなら、もはや 2人だろうが、9人だろうが関係ない。全てのセル(169 = 13* 13) で、100万回程度ゲームを実施して表を埋めれば十分収束する。(100万回もやると、ストレートフラッシュですら数百回は成立する)。一つの表で、1.6億回のゲームをするってこと。

アルゴリズムは特に難しくない

  • AKs などの組み合わせを決める
  • 上記を満たす二枚を固定する (A♥, K♠)。対称性からこうおいてよいのは自明。hparse
  • 100万回以下を実施
    • 上記の 2枚を抜いて 50枚のデッキをシャッフルして
    • ボードと、 n人のプレイヤーにカードを配り
    • 自分(A♥, K♠)が一番タイであればカウンタをインクリメント
  • 規格化(カウンタ値を1万で割る)して、マスの数字を埋める
const simulate = (hname, nPlayer, nSample) => {
  const hcards = hparse(hname);
  let counter = 0;
  for (let i = 0; i < nSample; i += 1) { // XXX: really big loop. performance over cleaness
    const deck = getdeck().filter(c => !hcards.includes(c));
    const myval = handval([hcards[0], hcards[1], deck[0], deck[1], deck[2], deck[3], deck[4]]);
    let winFlag = true; // to be false when any one of opponents wins
    for (let p = 0; p < nPlayer - 1; p += 1) { // nPlayer includes me, so opponents # is nPlayer -1.
      const ophandval = handval([deck[0], deck[1], deck[2], deck[3], deck[4], deck[5+2*p], deck[6+2*p]]);
      if (ophandval < myval) {
        winFlag = false;
        break;
      }
    }
    if (winFlag) counter += 1;
  }
  return counter / nSample * 100.0;
};

for (const nPlayer of [3, 4, 5, 6, 7, 8, 9]) {
  const stats = {};
  for (const name of HCARDNAMES) {
    stats[name] = simulate(name, nPlayer, 1000000);
  }
  console.log(JSON.stringify(stats));

結果

n = 2, 5, 9 の場合を載せとく。完全版は Githubに載せた

n = 2 は冒頭のテーブルを忠実に再現する。

n = 5 でも AA, KK, QQ は十分強い

n = 9。 UTG、 QQ で意気揚々とベットするのはちょっとのんきすぎる。

技術について

  • D3.js

可視化には必須。前の記事 でどのようにこのようなきれいな図を書けるのかを具体的なコードで提示。

  • シミュレータ

最近 github に公開した。 41semicolon/41poker しばらくは活発に動くのでAPIとかめちゃくちゃに変わるので注意。

考察と次へのステップ

とりあえず誰もがやるであろう計算をしただけなので考察をするに至っていない。これから少し考える。まあ、1/n 以下の勝率の手では参加させないというのがエージェントの基本政略の一つになるのだろうなあ。