アルゴリズムの力で、自作のカードゲームのAIを最強にした話

ゲーム開発技術記事競技プログラミング

対戦型ゲームを作った際に、botによる対戦を導入するケースは多くあります。対人による対戦が苦手なプレイヤーに対する遊び方を提供できますし、どんな場合でも必ずしもマッチング相手が見つかるとは限りません。

しかし、bot を作って導入してみたはいいものの、「あまり賢い立ち回りをしてくれない……」「botが弱すぎて、相手がbotだとわかった時点で実質勝ち試合になる……」といった悩みはありませんか?

今回の記事では、開発中のカードゲームに導入したbot対戦機能について、その開発の過程や背景にある思想などについてまとめてみました。

本投稿は、「ゲーム制作にかかわる人が自由な記事を作る Advent Calendar 2024」17日目の記事です。

スポンサーリンク

どんなゲームなの?

現在、「運任せの魔法学園」というゲームを開発中です。お互いの手番では、サイコロを振り、出た目を消費することでお互いにカードを発動させ、相手の体力を0にすることを目指します。

こちらのゲームは、2025年2月15日に予定されている「東京ゲームダンジョン7」にて展示を予定しています。興味がありましたら当日はぜひお立ち寄りください!

試合開始時に、何枚かの「カード」が場に登場し、これらは両者が共通して使える効果になります。一方、「サイコロ」はお互いの手元に存在し、それぞれのみが使えるリソースになります。ですので、通常のゲームに登場する「手札」にあたるのがこのゲームの「サイコロ」になります。

なんでAIが必要なの?

オンラインマッチングへのbot導入

オンラインマッチを実装しようとした際に、問題になるのはプレイ人口です。どの時間帯にも常にプレイヤーが参加しているとは限りません。そのため、マッチング相手が見つからなかった際には(仕方なく)botと対戦させる必要があると考えました。その際に、「botと対戦してるな~」とあからさまに思わせると体験の質の低下につながってしまうため、より洗練されたbotを導入する必要があります。

ランダムダンジョン

今後実装する予定の機能の一つとして、「ランダムダンジョン」があります。現在実装を進めているステージでは、対戦開始時にピックされるカードはすべて事前に定められた未定のものなのですが、ゲームクリアまで遊びきった後に提供できるコンテンツがない、という問題があります。

そこで、エンドコンテンツとして考案されているのが「ランダムダンジョン」です。「ランダムダンジョン」では、ピックされるカードがコンピューターの乱数によって決定されます。そのため、半永久的にいくらでもステージを追加することが可能になります。

敵のカード選択思考ルーチンは、現時点ではステージごとに固有のものが設定されています。(例えば、このステージでは最初にカードAを使う。まだサイコロが余っているなら、別のカードBを使う。といった様子)ただ、「ランダムダンジョン」にて実装されるステージではそういったステージ固有の思考ルーチンを実装することはできないので、どんなカードプールに対してもうまく機能する思考ルーチンを実装する必要があります。

AIに何を求めてるの?

今回のゲームでは、明確に達成してほしい要件があります。それが「リーサルを逃さない」ことです。

「リーサル」とは、「相手を倒しきることで、勝利を確定させることが出来る状態のこと」です。対戦型カードゲームにおいては、毎ターン攻撃側が入れ替わるため、「このターンの自分の攻撃で相手を倒しきることができるか?」は極めて重大な要素となります。言い換えれば、「リーサルを逃さない」ということが強さの最低条件ともいえるわけです。

どんなAIなの?

前置きが長くなりましたが、ここからアルゴリズムの実装の話になります。

概要

とてもざっくりと概要をまとめると、「いろんな行動パターンのうち、『最適っぽそうなもの』を選ぶ」というロジックです。以下では、各部分に分けて説明していきます(説明の都合上、細かい話を省略している部分があります)。

状態の管理

「状態」とは、局面の状態のことです。局面を評価するために必要な指標はほぼすべて含まれます。例えば、

  • 自分 / 相手の残りHP
  • 自分 / 相手の所持サイコロ数
  • 各サイコロに付与された状態異常 / 上限・下限の値

ここで重要なのは、ゲームプレイ中に保存され、実際にプレイ画面に表示される「状態」とは異なるものであるということです。行動を決定するためのシミュレーションのために用いられる、いわば「仮想的な局面の状態」と思ってください。

状態の評価

局面の状態が与えられた際に、それがどのくらい自分にとって望ましい状況なのかを意味する数値を算出する関数を定義します。(この値のことをスコアと呼びます。)この値が出来るだけ高くなるように、コマンドやサイコロを選んでいく、ということになります。

スコが高くなる条件の一例には、次のようなものがあります。

  • 自分の残り HP が高い / 相手の残り HP が低い
  • 自分の所持サイコロが多い / 相手の所持サイコロが少ない
  • 相手に掛けられている状態異常が多い / 自分に掛けられている状態異常が少ない
  • (高い出目を活かせるカードが多い場合)自分のサイコロの出目の期待値が高い

また、前の章でも述べた「リーサル」の話もこの部分で考慮されます。つまり、「相手の残り HP が0になるような状態」に対するスコア評価が非常に高くなるようにします。そのことにより、このターン中に相手を倒しきれるのであれば、その行動を絶対に優先する、という思考ルーチンになります。

カード・サイコロの選択

ターン終了時点での状態のスコアが高くなるように、カードとサイコロを選択します。

ここでは、動的計画法という手法を導入しています。動的計画法自体の説明をこの記事ですると長くなってしまうので、Web上の記事などを参照してください 1 2。以下ではざっくりと概要だけ説明します。

具体例を持ち出してみましょう。ターン開始時にサイコロを3つ振って「1」「2」「3」の目がそれぞれ出て、利用できるカードが以下の3種類だったとします。

  • サイコロを1つ消費して、相手に30ダメージ
  • 奇数のサイコロを1つ消費して、相手に50ダメージ
  • 合計が4以下になる2つのサイコロを消費して、相手に90ダメージ

このときに、使うサイコロの組み合わせごとに、それらで与えることの出来る最大ダメージについて考えてみましょう。

全くサイコロを使わない場合、この場合は当然0ダメージです。

「1」のサイコロだけ利用する場合、2個目のカードを使うことで 50 ダメージを与えることができます。この情報を表にメモしておきます。

「2」のサイコロだけ利用する場合は、1枚目のカードしか使えないので、30 ダメージです。この情報も、表にメモしておきます。

「1」「2」の2つのサイコロを使う場合、考えられる方法は3通りです。

  • 「1」のサイコロだけ利用した場合(すでに 50 ダメージを与えている。この情報はメモしてあるのですぐ取ってこれる)から、「2」のサイコロを利用する(追加で 30 ダメージ)。この場合は合計 80 ダメージ。
  • 「2」のサイコロだけ利用した場合(すでに 30 ダメージを与えている。この情報はメモしてあるのですぐ取ってこれる)から、「1」のサイコロを利用する(追加で 50 ダメージ)。この場合も合計 80 ダメージ。
  • 全くサイコロを使わない場合から、「1」「2」のサイコロを使って3枚目のカードを使って 90 ダメージ。

この結果、「1」「2」の2つを使う場合の最適値は 90 ダメージだとわかります。この情報も表にメモしておきます。

……という要領で表を埋めていくことができ、最終的な最適値が 140 ダメージであると求めることができます。

これで全部うまくいくんですか?

現状のアルゴリズムには少し問題点があります。

「今後のためにいったん我慢」が苦手

長期的に見たときに最適な結果を生み出したい場合、短期的には「良くない」とされる行動をとることが必要となる場合があります。例えば、以下のようなカードが場に存在したときのことを考えてみましょう。

  • サイコロを1つ消費して、相手に 30 ダメージ。
  • サイコロを1つ消費して、相手に N × 10 ダメージ。(このカードを使うのが N 回目の場合)

以上のような状況では、2回使えば2つ目のカードが 30 ダメージを出せるようになります。また、長期的に見れば6回目以降からは2つ目のカードのほうが得をします。

ただ、現状のAIは「カードを使った直後の状態」しか考慮に入れてないため、1つ目のカードを延々と使い続けてしまいます。(お米3㎏を買うと高いから、コンビニでお弁当買い続ける、みたいなイメージ)

「どのような状態が理想的なのか?」は結局人間が考えている

カードの効果には、単純に相手にダメージを与えるもの以外にも様々なものがあります。相手のサイコロにデバフをかけたり、逆に自分のサイコロにバフをかけたり、自分が次のターンに受けるダメージを減らす防御スキル、などなど……

こういった直接攻撃以外のカードを評価するのは難しいです。スコアに反映させるために、最終的には数値で評価することになるのですが、(例えば、防御1つにつき+〇〇点、など)この数値設定はAIを作る際に人間が手打ちで数値を入れています。この値によってAIの行動が変わってくるのですが、現状が最適な設定になっているのかはわかりません。

サイコロがいっぱいあると、考えるのに時間がかかる

前章で説明したアルゴリズムは、指数時間アルゴリズムと言って、処理するべき情報(特にこの場合だと、手持ちのサイコロの数)が増えると、それに伴って計算にかかる時間が急速に増えていきます。

このゲームだと、状況によっては所持するサイコロが 10~20 個まで増えることはままあるため、そのままこのアルゴリズムを適用することはできません。その場合には少し特殊な別処理をして計算にかかる時間を減らしています。

最後に

ゲームの中身のロジックについて詳細に説明するような記事を書いたことがなかったので、どんな需要があるのかよく分からないまま書いてみました。

「何言ってるのか分からなかった!」「ここがもう少し知りたかった!」「実際のコードが見てみたい!」など、どんな種類のフィードバックでも良いので、ぜひお聞かせくださればと思います。(記事へのコメントでも良いですし、Xへの反応(リプライ・DM・引用RPなど)も大歓迎です!)需要がありそうであれば今後もゲーム開発に関するアルゴリズム関連の記事を書いてみようと思います。

繰り返しになりますが、本ゲームは2025年2月15日に予定されている「東京ゲームダンジョン7」にて展示を予定しています。興味がありましたらぜひお立ち寄りください!

みんなで待ってるよ!
  1. https://algo-logic.info/dynamic-programming/ ↩︎
  2. https://www.momoyama-usagi.com/entry/info-algo-dp ↩︎

コメント