はじめに

※このエントリは完全に人間が書いています。

パズルゲーム「Stardust Sweeper」を作りました。

タイトル画面

このゲームは前作Hodokuと同様にAIを駆使して二日で作ったのですが、いろいろ苦労したのでそのあたりの話をまとめておきます。

ゲーム開発とAI

僕はもともとフリーソフト作家であり、ゲームもいくつか作って公開していました。しかし仕事が忙しくなり、ゲームのアイデアはあるものの、ゲーム開発に割く時間が取れずにそのままになっていました。

しかし、今年からCodexを使うようになり、コード開発が加速しました。また、夏休みに娘にせがまれ、一緒にホラーゲームを作ったのですが、それが案外簡単にできたため、またゲームを作ってみようと思って作成したパズルゲームがHodokuです。

Hodokuは、ウェブ版ChatGPTと相談して仕様をまとめ、その仕様をCodexに食わせる、というやり方で作りました。寝る前にChatGPTと対話、次の日にCodexに食わせ、家事なり子供の面倒を見ている合間に確認しては指示する、という形で、片手間であっというまにゲームができてしまいました。BGMはSunoのProプランで、また寝る前にプロンプトを投げていくつか作り、アセットもChatGPtに作らせて、それを次の日にゲームに組み込んで完成、という流れです。ゲームを作ろうと思ってからリリースまで2日でした。しかもがっつりPCに張り付く必要もなく、働くCodexの様子を見てたまに指示するだけでした。

さて、これを見て、以前作ったゲームのリメイクをしたくなりました。まず作ろうと思ったのは、「穴掘りWars 2」というゲームです。

anahori

このゲームは、はしごを上ったり、ブロックを押したり、土を溶かしたりしながら「愛」までたどり着くゲームです。DOSで試作し、Windowsで公開しました。窓の杜にも紹介されています。

【連載】週末ゲーム 第38回:穴掘りパズルゲーム「穴掘りWars2」

公開は2000年です。

さて、Hodokuにおいて、パズルはC++で自動生成し、ゲームはGodotで作る、というのがうまくいったので、これもすぐできるだろうと思って作ってみたのですが、なかなかうまくいきません。

いろいろ試してみて、Hodokuに比べて、穴堀りゲームがAIには難しいことがわかりました。

まず、ルールが明らかに複雑です。Hodokuは、格子点上にある宝石をタップで交換するだけで、状態はほとんど変わりません、しかし、穴掘りゲームは、キャラクターが上下左右に移動し、落下し、石を動かし、土を消し、上からブロックが落下します。この仕様をきっちり決めてAIに理解させるのに非常に苦労しました。

元のゲームでは、左右の土を溶かした時に、上にブロックがあると落下するが、なければ次は落下しない、上にブロックはないけど、空白の直上にハシゴがあるとハシゴだけ落下する、といったルールでしたが、何度説明してもAIが理解できません。土を溶かして空白ができたとき、

土
土
空

という状態になり、元のゲームではそのまま上空の土は保持されるのですが、AIはどうしても「土が落下する」と判定してしまいました。自分でプログラムを組むならさほど難しくなかったのですが、今からGodot 4スクリプトを覚えてロジックを組むのはちょっと無理でした。

また、もう一つ難しかったのが、キャラクターアセットの作成です。ほとんどの場面でマス目単位のキャラクターを生成すればよいのですが、「ブロックを押す」「土を溶かす」というのは、2マスにわたるエフェクトが必要になりました。このような、複数マスにわたるエフェクトの生成は(少なくとも僕が試した範囲では)AIは非常に苦手で、ブロックが別にあるのに「ブロックを押すキャラクター」を生成してしまったり、目から怪光線を出して土を溶かすのに、その光線が目と合わないなど、どうしてもきれいにキャラクターを作れませんでした。

結局、ステージ作成コードや、テストプレイ用のコードまで作ったのですが、ここでお蔵入りになりました。

夏休みも終わり、次の週の土日、僕はもう一つのゲーム、「星工房」のリメイクに挑戦します。

hoshi

これは、「ぷよぷよ」と「倉庫番」を組み合わせたようなルールのパズルゲームで、ブロックを各色4つずつつなげてすべて消すのが目的です。

ちなみにこれも窓の杜に紹介されたりしています。

同じ色のブロックを4つ並べて消すパズルゲーム「☆星工房☆」v0.80β

2001年1月ですね。当時M2。修論で忙しいはずの時期に何やってるんだか。

さて、このゲームは、基本的に状態変化はブロックの移動と消滅だけであり、複数マスにわたるエフェクトは必要ありません。これなら作れるだろうと開発に着手しました。

開発方針

基本方針はHodokuと同様に、

  • C++でパズルを大量に自動生成する
  • 自動生成されたパズルからよさげなものを選んで配置する
  • ゲームのテストプレイやゲーム本体はGodot 4で作る

というものです。しかし、Hodokuほど簡単にはいきませんでした。

パズルの自動生成

このゲームには、壁、4色のブロック、星ブロックがあります。星ブロックはオールマイティで、例えば「赤赤赤星青青青」のようなパターンで、星は「赤でもあり青でもある」と解釈されて全て消えます。

また、Windowws版には無い「透明ブロック」を追加しました。これは「おじゃまぷよ」と同じで、それ自体は消えないけれど、隣接するブロックが消えるとそのブロックも消えるというものです。

最も単純には、これらをランダムに発生させて、問題を解かせてみて、解があったらそれを出力する、というものを繰り返すことです。しかし、この方法では解空間が広すぎて時間がかかりすぎました。

特に、まったくランダムにブロックを生成すると、ある色のブロックが3個しかないような、絶対クリア不可能な状態ばかり生成されてしまいます。また、星ブロックはオールマイティですが、例えば他に一色しかブロックがない時に星ブロックがあっても、それはその色のブロックと同じ役割になってしまい、星ブロックの意味がありません。星ブロックは、複数の色のブロックがあってはじめて意味を持ちます。

いろいろ試行錯誤しましたが、「いい感じのブロック数」を自動生成することができず、結局「ユーザがどのブロックを何個生成するかを完全に指定する」という方法に落ち着きました。

例えば「星1個、色ブロック3,3」と指示すると、星1, 赤3,青3など、色だけランダムに選ばれて、場所はランダムに配置されます。

さらに、壁の位置の配置もランダムではうまくいきませんでした。単純に壁もブロックもランダムに配置すると、ほぼ間違いなく解が存在しません。

そこで、「まず壁なしでブロックを配置、そして解を調べ、その解を邪魔しないように壁を配置」する形にしました。

オリジナルのWindows版では、わりと広い空間に多数の色ブロックがあるようなステージが多かったのですが、自動生成したゲームでは壁が多めのステージが多く生成されるようになりました。

見た目の美しさにこだわったステージ作成ルーチンも作ろうかと思いましたが、ちょっと時間がなくてあきらめました。

あとは細かいことでですが、最初にAIが実装してきたコードがメモリを際限なく使ってしまうため、状態保存をキューにして上限サイズを決めたり、あまりにも遅いためにゲームのルールに即した枝刈りの実装を指示する必要があったりしました。

例えば、「上と右」や「左と下」など、直行する二方向で壁に接したブロックはもう動かすことができません。二つ以上「動けなくなったブロック」があり、そのマンハッタン距離がその色のブロック(+星ブロック)の数よりも小さければ、この盤面はクリア不能なので枝刈りができます。

こういった細かい枝刈りを指示することで、ようやく実用的な速度が出るようになりました。それでもオリジナルのゲームにあったような、色とりどりのブロックが大量に配置されるようなステージを作らせることはできませんでした。このあたりはもう少しなんとかしたいものです。

難易度の自動判定

このゲームのキモになるはずだった機能です。Hodokuでも感じたのですが、パズルの自動生成は非常に簡単である一方、それを「人間にとっての難易度」でいい感じに並べるのが非常に困難です。どうしても「ものすごく簡単」と「ものすごく難しい」のどちらかに偏ってしまい、難易度曲線にジャンプが生まれてしまいます。

そこで、強化学習エージェントにより難易度推定をさせることを試みました。PPO(Proximal Policy Optimization)を用いてエージェントを訓練し、問題を解けるようにして、訓練済みエージェントがどのくらいの確率で問題を解けるかを参考にして難易度判定を試みました。

しかし、このゲームのルールは複雑であり、エージェントの訓練は困難を極めました。エージェントは、状態を受け取って、方策として「上下左右」を決定します。最初は「壁に向かって動こうとするなど、無効な動作をしないように覚えさせよう」と、無効な動作にペナルティを課しましたが、エージェントは「無駄に左右に動き続ける」など、「ペナルティを回避する行動」を重視するようになってしまいました。そこで、Action Maskをつけ、無駄行動はできないようにしたところ、問題を解けるようになりました。

しかし、問題が少しでも複雑になると、急に解けなくなります。例えば、ある色ブロック4つだけのステージは解けるようになるものの、星ブロックや透明ブロックが出てきた瞬間にどうにもならなくなりました。

そこで、ステージを供給するfeederに「これまでに解けるようになったステージ」を多めに出題させ、たまに「解けないステージ」を混ぜることで、エージェントに成功体験を多く積ませることで、解けないステージも徐々に解けるようになってきました。

こうして段階的に学習を進め、簡単な問題なら解けるようになってきましたが、それでも安定して解ける(正答率50%以上出せる)のはせいぜい5手問題程度であり、10手となると正答率10%を切り、さらにブロックの種類が増えると手も足も出なくなりました。

問題の自動生成により7000問ほど作り、その中で100手以上を「高難易度ステージ候補」とする予定で、特に「最高難易度」は200手を超えるステージでした。これを人間の手で難易度判定するのは非常に手間なので、そこを強化学習でなんとかしたかったのですが、10手でわたわたしているエージェントには200手の問題などどうしようもありません。今回は強化学習エージェントを難易度判定に使うことは断念しました。

このあたり、まだ強化学習の経験値が足りないと痛感しました。

結局、最小ステップ数を軸に、ブロックの種類などでヒューリスティックな難易度推定器を作り、配置しました。実際に解いてみると、ところどころ難易度のギャップがあるため、あまり満足のいく結果になりませんでしたが、7000問をテストプレイして100問選ぶというのは(普通の勤め人が余暇にやるのあ)不可能なので、今回はこれで妥協しました。

まとめ

AIを使ってパズルゲームを2つほど作ってみました。どちらも「ChatGPTに仕様の相談をはじめてから、itchでのリリースまで」は日でできました。すごい時代になったものです。

しかし、Hodokuではハミルトン閉路を作るために、まず全域木を作って、それをハミルトン閉路に変換し・・・などとやって、Stardust Sweeperでは、問題作成ルーチンのメモリ管理、枝刈りの指示が必要であり、「単純にゲームのアイデア一発だけ」でゲームを作れたわけではありません。

僕はもともとゲームを作っていた経験があったので、AIが詰まったときに「こうすればよいのかな」という「アタリ」をつけることができましたが、最初からAIを使った開発から入った若い人はどうやってこういう知識を身につけるのだろう、と思ったりしました。

いずれにせよ、AIによってさまざまな開発が加速するのは間違いありません。その勢いにふるい落とされないように老体にむち打ちって頑張っています。

おまけ

記事中に出てきた「穴掘りWars2」は、FLASHでもリメイクしました。以下のページで遊べます。

穴掘りWars 2

FLASHのサポートが切れ、「せっかく作ったのになぁ」と思っていたのですが、Ruffleによるエミュレータで動きました。