ASP.NET Core版 対コンピュータ対戦できるぷよぷよをつくる(5)の続きです。前回はAIの基礎部分を実装しました。今回はそのなかから最適な選択をさせます。

評価点の計算法

ぷよは現在落下しているものと次回、そして次々回のものがわかっています。またひとつの組ぷよの置き方はどの列に配置するかと回転量をどうするかで全部で22通り存在します。そのため現在落下している組ぷよと次回、次々回のぷよが配置された結果として22の3乗個の結果が存在します。

では評価点はどのようにして計算すればいいでしょうか?

この場合、Bのぷよが消えると上にあるぷよが落ちてきて連鎖が発生します。そこで各ぷよを調べてそのぷよの下に同じ色のぷよが存在する場合は評価点を加点します。またそのぷよの下にあるのは異なる色のぷよではあるが、それが1種類の色しかない場合も加点の対象とします。いずれの場合も同じ色のぷよのさらに下にあるぷよが同じ色である場合はさらに加点することにします。

この場合もBのぷよが消えると上にあるぷよが落ちてきて連鎖が発生します。そこで各ぷよの隣の列のそのぷよの下側に同じ色のぷよが存在する場合は評価点を加点します。同じ色のぷよのさらに下にあるぷよが同じ色である場合はさらに加点することにします。

今回はこの22の3乗個の結果に対して評価点を与え、そのなかで評価点がもっとも高いものを採用することにします。これでデタラメに置くよりも連鎖が発生する確率が高くなることがわかりました。鳩でもわかるC#管理人にはもう太刀打ちできません。しかしぷよぷよが好きな人にとってはそうでもないらしく簡単に撃破されてしまいます。このあたりはもう少し強いアルゴリズムを実装するために研究することにします。

Fieldクラスに機能を追加する

コンピュータ側のアルゴリズムを考えるうえで既存のFieldクラスに機能を追加します。

以下はフィールド上に固定されているぷよの二次元配列のコピーを取得するプロパティです。組ぷよを落としたときのパターンを取得したいのですが、実際に固定されているぷよの二次元配列を直接操作すると状態が変わってしまうのでコピーを取得してこれを操作します。

GetFallingPuyosメソッドは、現在落下中の組ぷよとネクストぷよを取得するためのものです。

落下中のおじゃまぷよのコピーを取得します。

AiVer1クラスの定義

3組の組みぷよで考えられる組み合わせをすべて取得し、そのなかから最適と思われる行動をするクラスを実装します。

以降は名前空間を省略します。

コンストラクタ

コンストラクタを示します。引数はふたつあるフィールド(PuyoGame.Fields[0]とPuyoGame.Fields[1])のどちらを担当するかです。

ぷよを落とした結果を総当たりで取得する

GetAiResultsメソッドは3つの組ぷよを落とせるところに落とした結果を総当たりで取得します。

ぷよが上まであがってくると組ぷよを落とせる場所が0になる場合があります。この場合は空のリストが返されます。

負けそうになったときの行動

基本的にぷよを消さずに大連鎖を狙いますが、ぷよが上のほうに上がってきている状態では1連鎖であってもぷよを消すことを優先します。以下はそのような危険な状態にあるのかを判別するメソッドです。

危険な状態でコンピュータが取るべき行動のリストを取得する処理を示します。

連鎖が発生する場所を探す

少なくとも第二引数回数の連鎖が発生する行動を探す処理を示します。

連鎖する場合をみつけた場合、早い段階で連鎖するものを優先したいのでこのような処理にしています。

コンピュータ側にとって最適な行動を取得する

最適と思われる行動を選択する処理を示します。

すでに窒息している場合は最適の行動は存在しません。それ以外の時はGetAiResultsメソッドを呼び出してとりうるすべての行動のリストを取得したあと、以下の処理をおこないます。

まず現在危険な状態なのかを調べます。危険な場合はActionOnDengerousメソッドを呼び出して危険回避の行動を探します。

それ以外のときは連鎖数が4以上のものがあるか探します。見つからない場合はそこに置いてもぷよが消えず危険な状態にならない場所を探します。それが存在しない場合は窒息しない場所を探します。

評価値の計算

評価値を計算する処理を示します。

ある位置にあるぷよの下に同じぷよがある場合、異なる色のぷよがあってもその下に同じ色のぷよがあり、間にあるぷよの色が一色の場合は加点します。また下にあるぷよの下にさらに同じ色のぷよがある場合はさらに加点を増やします。

実際にぷよを移動させて落とす

最適と思われる行動を選択し、実際にそのようにぷよを移動させて落とす処理を示します。

最適解を得るための処理が重いので取得できた3回分の行動をまとめて行なうことで処理が重くなるのを防ぎます。ただサーバー上で動かしてみると動作は重いです。複数のユーザーがプレイしている場合はさらに重くなり、ゲームとしては成り立たなくなっています。この部分は後日改善します。

またコンピュータ側の2手目と3手目がはじまるタイミングでおじゃまぷよが降ってくる場合があります。コンピュータ側の最善手はそのおじゃまぷよは存在しないという前提で計算されているので、この場合はおじゃまぷよが存在を前提に最善手を探す処理をもう一度やり直さなければなりません。そのためCancelメソッドを定義しています。

自作のAIを動作させる

定義したクラスのインスタンスをPuyoGameクラス内に生成します。またField.OjamaFalledにイベントハンドラを追加していますが、これはコンピュータ側のフィールドにおじゃまぷよが降ってきたときに最善手を探す処理をもう一度やり直せるようにするためです。コンピュータ側の2手目と3手目がはじまるタイミングでおじゃまぷよが降ってくるとAIは新しくフィールド上に出現したおじゃまぷよの存在を知ることができないので、このようにしています。

RunAiメソッドのループのなかでSystem.GC.Collect()を呼び出しています。コンピュータ側の手を探すために短時間で大量のAiActionResultオブジェクトを生成しているため、これらを回収するために呼び出しています。最初はこれに気づかなかったため、サーバーのメモリー消費量がヤバいことになっていました。

PuyoGameクラスに追加するメソッドと既存のメソッドで修正する部分を示します。

ゲーム開始時に上記メソッドを呼び出します。またコンピュータ側がゲームオーバーになったときはステージクリアであり、これにともなってRunAiメソッドも終了するので再度RunAiメソッドを呼び出します。