さくさく理解する Godot 入門(ただし2Dに限る)応用編 テキサスホールデムポーカー【第6回】
目次
- ポーカーAI
- 正直AI
- スモールブラフAI
ポーカーAI
前章で、期待勝率を計算するコードを示した。期待勝率が計算できると、 原始的なAIを実装することが可能になる。期待勝率が高ければ強気の行動を、低ければ弱気の行動を選択するとよいだけだ。
純粋にそのように行動を選択するAIを本稿では「正直AI」と呼んでいる。
実際にカジノなどで本格的なポーカーをプレイした人は知っていると思うが、常に正直な行動を選択していると、 相手に手札がすぐにバレてしまう。 弱い手札の場合であってもたまにブラフ(はったり)でレイズしたり、 強い手札でも様子見でチェックする方が相手に手を見破られずらくなるし、弱い手札でのブラフなのに相手が降りたり、 強い手札のときに相手が降りずにコールしてきてチップを余計にゲットすることもあるのだ。 ブラフが大胆すぎると、単に何やっているかわからない弱いAIになってしまう。 本稿ではブラフを控えめに行うAIを「スモールブラフAI」と呼んでいる。
例えば、AI の手札がAのペアのようにかなりいい手だった場合、「正直AI」であれば AI は強気の行動であるレイズを常に選択する。
だが、「スモールブラフAI」の場合は、強い手札であってもレイズを常に選択するわけではなく、 少ない確率で様子見であるチェックを選択する。
逆に AI の手札が非常に弱い場合、「正直AI」であれば AI は弱気の行動であるフォールドまたはチェックを常に選択するが、 「スモールブラフAI」の場合は、少ない確率でブラフでレイズすることもある。
正直AIの方がいいのではないかと思われる人もいるかもしれないが、ポーカーは相手との手の読み合いなので、 常に正直な行動を取っていると、トータルで勝利することはできないのだ。
■正直AI
下記に正直AIのコードを示す。
func do_AI_action_honest(pix, max_raise): var wrt = calc_win_rate(pix, nActPlayer - 1) # 期待勝率計算 print("win rate[", pix, "] = ", wrt, " (*", nActPlayer, " = ", wrt * nActPlayer, ")") #print("wrt = ", wrt) print("bet_chips_plyr[", pix, "] = ", bet_chips_plyr[pix]) if( max_raise > 0 && wrt >= 1.0 / nActPlayer * 1.5 && # 期待勝率が1/人数の1.5倍以上の場合 n_raised < MAX_N_RAISES ): # 最大レイズ回数に達していない場合 var bc = min(max_raise, max(BB_CHIPS, int((pot_chips + cur_sum_bet) / 4))) do_raise(pix, bc) elif bet_chips_plyr[pix] < bet_chips: # チェック出来ない場合 var cc = bet_chips - bet_chips_plyr[pix] # コール必要額 var odds = float(pot_chips + cur_sum_bet + cc) / cc # オッズを計算 print("total pot = ", (pot_chips + cur_sum_bet), " odds = ", odds) var wrt_odds = wrt * odds if state == PRE_FLOP && wrt_odds >= 0.66 || wrt_odds >= 1.0: print("called") do_call(pix) else: do_fold(pix) else: # チェック可能な場合 do_check(pix)
まずは、期待勝率を計算し、レイズ可能かつ期待勝率が1.0を降りていない人数で割った値の1.5倍以上であればレイズを選択する。
例えば、5人プレイヤーが残っている場合は、期待勝率が2割あれば勝てるかどうかトントンということになる。 なので、1.0を降りていない人数で割っているのだが、少しくらい有利でレイズするのは危険もあるので、 期待勝率と商の 1.5 倍とを比較している。
次に誰かがレイズしていてチェックできない場合は、オッズを計算し、期待勝率がオッズに見合う場合はコールを、 見合わない場合はフォールドを選択する。
オッズとは掛け金を払った場合、勝つと何倍になって返ってくるかという値だ。 例えばポットに8チップが出ていてコールに2チップ必要な場合、賭けに参加するにはチップ2が必要だが、 勝つとチップ10を貰えるので、 オッズは5倍となる。したがって、勝つ確率が20%以上なら降りずに賭けに参加する方がよい。という考え方だ。
AI は行動を決めると do_raise(pix) などのその行動を行う関数をコールする。これらの説明は後の章で行う。
■スモールブラフAI
下記にスモールブラフAIのコードを示す。
func do_AI_action_small_bluff(pix, max_raise): var wrt : float = calc_win_rate(pix, nActPlayer - 1) # 期待勝率計算 print("win rate[", pix, "] = ", wrt, " (*", nActPlayer, " = ", wrt * nActPlayer, ")") print("bet_chips_plyr[", pix, "] = ", bet_chips_plyr[pix]) var wrtnap = wrt * nActPlayer # 期待勝率 * アクティブプレイヤー数 var pr_check_call = 0.0 var pr_raise = 0.0 if max_raise > 0 && n_raised < MAX_N_RAISES: if wrtnap >= 1.5: pr_raise = 0.9 # レイズ確率 pr_check_call = 0.1 elif wrtnap >= 1.0: pr_raise = 0.2 + (wrtnap - 1.0) / 0.5 * 0.7 # レイズ確率:[0.2, 0.9] pr_check_call = 1.0 - pr_raise elif wrtnap >= 0.5: var t = (wrtnap - 0.5) / 0.5 pr_raise = t * 0.2 pr_check_call = t * 0.8 var pr_fold = 1.0 - pr_raise - pr_check_call print("prio rase = ", pr_raise*100, "%, call = ", pr_check_call*100, "%, fold = ", pr_fold*100, "%") var r = rng.randf_range(0, 1.0) if r <= pr_raise: # レイズを行う var bc = min(max_raise, max(BB_CHIPS, int((pot_chips + cur_sum_bet) / 5))) do_raise(pix, bc) elif r <= pr_raise + pr_check_call: # チェック or コールを行う do_call(pix) else: do_fold(pix)
正直AIの場合同様に、まずは calc_win_rate() をコールして期待勝率を計算する。 それにフォールドしていないプレイヤー数を乗じたものを変数 wrtnap に格納し、 それをもとにレイズ・チェック/コール・フォールド確率を決めている。 wrtnap が 1.5以上であればレイズ確率を 0.9、wrtnap が 1.0 であればレイズ確率を 0.2 とし、 その間を線形補間している。 wrtnap が 0.5 であれば、レイズ確率を 0.0 とし、その間を線形補間する。 同様にチェック/コール確率も計算する。 この計算式は特に根拠は無い。
で、最後に乱数を発生させ、レイズ確率・チェック/コール確率をもとに行動を選択している。
このAIは手札の強さを考慮した上で最終的に乱数で行動を決めるので、手を読みづらいという利点はある。 だが、その分強いかと言うと、残念ながらそうでもない。特に行動に首尾一貫性がないのが致命的だ。
比較的自然な行動でなおかつ強いポーカーAIを作るのはなかなか難しい。
TechProjin Godot入門 関連連載リンク
Godotで学ぶゲーム制作
さくさく理解するGodot入門 連載目次
標準C++ライブラリの活用でコーディング力UP!
「競技プログラミング風」標準C++ライブラリ 連載目次