はじめに、または最初のAIを書いたとき
良い一日。 私が大学にいたとき、私は何年も前に私の最初の人工知能を書きました。 それはヘビの珍しいゲームであるヘビのAIでした-
ヘビの狂気 (リンクは私のゲームWebサイトにつながります)で、後者は任意の方向に移動できます。 以下のスクリーンショットはこれを示しています。

そして、それは決定論的アルゴリズムでした。 アクションの明確なシーケンスを備えたアルゴリズム。各ステップで次に何が起こるかを正確に伝えることができます。 こんな感じでした
(擬似コード):
rwarn = 0 //右側のヘビに対する危険
lwarn = 0 //左側のヘビに対する危険
すべての敵に対してop {
ポイントが敵の骨の中心である場合、opはヘビの半円内にあり、{
ポイントがヘビの右側にある場合、
rwarn ++;
ポイントがヘビの左側にある場合、
lwarn ++;
}
座標(xまたはy)の1つに沿った動きベクトルの最大偏差で、指定された距離以下で壁の1つに平行に移動する場合、{
壁がヘビの右側にある場合、
rwarn + = 100;
壁がヘビの左側にある場合、
lwarn + = 100;
}
rwarn!= 0およびlwarn!= 0の場合、{
rwarn> lwarnの場合、左に曲がります。 それ以外の場合は、右側に。
}
} //すべての対戦相手op
つまり このAIは、ヘビの特定の場所に対して常に同じことを行います。ヘビや壁が少ない側に向きを変えます。 はい、はい、マジックナンバー「100」を示しました。これは壁がある場合に危険を増加させます。 マジックナンバーは悪いプログラミングスタイルですが、当時は許されませんでした。 これは、ヘビが壁よりも他のヘビに頻繁に衝突するようにするためです。条件はかなり相対的です:ヘビの半円内に他のヘビの骨(=パーツ)が100を超える場合、アルゴリズムは壁に衝突することを選択します。 これにもかかわらず、アルゴリズムは非常にうまく機能しました.AIはさまざまな角度でヘビを一周し、壁を一周し(他のヘビがそれを強制しない限り、壁に衝突しませんでした)、実行する必要があるときに壁とヘビの間でバランスを取りました。
ただし、彼には2つの欠点がありました。
1)ヘビが壁の近くを移動していて、AIが壁とヘビの間にあったとき、どこかに行くべき場所があったとしても、次のことが定期的に発生しました:AIは混乱し、けいれんしました-左に少し、右に少し、左に少し...そして死にました。
2)ヘビから同じ所定の距離で、AIは常にターンを開始しました。 ヘビがAIから遠く離れていて、少し遅れて-はるかに短い距離(鋭くAIに向かっている)であることが判明した場合、AIはクラッシュしました。 彼が前に曲がることができるか、少なくとも曲がり始めることができれば。 大きな半径を持つ別の半円を導入することを考えていました。そのために「少し」回す必要があります。 それから私は自分にやめた アルゴリズムが複雑になってきたように思えます。 複雑にする場合は、確かにウェイポイントを入力してください-私は思った。
これらの2つの欠点は一言で表すことができます-場合によっては、ヘビは「痙攣」し、そのために時々死にました。
5年後、私は
Serpent's Madnessをandroidに移植したとき、この欠点に対処することにしました。 そして、「ファジーロジック」がこれを助けてくれました。 ファジーロジックを、アルゴリズムに「ファジー」推論を導入するためのツールとして理解しています。 それでは、新しい視点からタスクを見てみましょう。
原理
Serpent's Madnessゲームのヘビは、左、右、または前方に移動できます。 彼女はまったく動けません。 したがって、AIには次の出力があります。
1)左に曲がる
2)右折
3)前進する
これに応じて、自然言語または人工言語のフレーズの意味をとることができる変数-言語変数を含む表を提示します。
| ヘビまでの距離 | ヘビ密度 | 壁までの距離 | 角を打つ |
左側に | | | | |
右へ | | | | |
先に | | | | |
セルに用語があります。 用語では、変数のフレーズのまさに意味を意味します(言語学的)。
遠く、ちょうど近い-距離に関しては(列1および3)
いいえ、小、中、たくさん-ヘビの密度に関しては(列2)
いいえ、おそらく確かに-コーナーに入るとなると(列4)
各用語に対して、メンバーシップ関数が定義されています。 意味的には、これらは0から1までの「危険関数」であり、値が大きいほど、特定の方向に移動する危険が大きくなります。
行iごとに、セルm(i)の最大値を計算します。 それでは、与えられた方向のパラメーターに関して最大の危険性があるとしましょう。 次に、そのようなすべてのm(i)から最小値を見つけます。 少なくともこれが質問に対する答えになります-何をすべきか、左/右に曲がるか、まっすぐに進みます。
以下にいくつかの例を示します。 私は、数値解釈が例としてのみ与えられているという事実に注目します。 実際、開発されたシステムでは、他の値がある場合があります。
例1
| ヘビまでの距離 | ヘビ密度 | 壁までの距離 | 角を打つ |
左側に | ちょうどいい | 少ない | いや | いや |
右へ | 遠く | いや | ちょうどいい | 確かに |
先に | 閉じる | たくさん | 遠く | いや |
結果は、左折する決定であるはずです。
計算から得られるもの:
min(max(0.5、0.33、0、0)、max(0、0、0.5、1)、max(1、1、0、0))= min(0.5、1、1)= 0.5 =左折
例2
| ヘビまでの距離 | ヘビ密度 | 壁までの距離 | 角を打つ |
左側に | 閉じる | 中 | 遠く | いや |
右へ | 遠く | いや | 遠く | いや |
先に | 閉じる | 中 | 遠く | いや |
結果は右折の決定であるはずです。
min(max(1、0.75、0、0)、max(1、0.75、0、0)、max(0,0,0,0)= min(1,1,0)= 0 =右折
例3
| ヘビまでの距離 | ヘビ密度 | 壁までの距離 | 角を打つ |
左側に | 閉じる | 中 | 遠く | いや |
右へ | 遠く | いや | 閉じる | いや |
先に | 閉じる | 中 | 遠く | いや |
その結果、決定を下すことは困難です-どこでも差し迫った死の危険があります。
ヘビが決めること:
min(max(1、0.75、0,0)、max(1、0.75、0、0)、(0,0、1、0))= min(1,1,1)= 1 =左折
私の観点では、次のことを論理に追加したいと思います。ヘビの同じ用語(近いとしましょう)は、壁よりも所属の度合いが低くなります。 これにより、差し迫った死で、壁ではなく、ヘビとのあらゆる方向の衝突を与えることができます-これはプレイヤーがより速くポイントを獲得できるようにするためです。
計算のルール
開発時には、次の点に注意します。 CPU時間を節約します。
1)max関数の項が計算され、1に等しい場合、残りを計算するポイントはありません。最大値は1になります。
2)min関数の項が計算され、0に等しい場合、残りを計算しても意味がありません。minは0を返します。
グラフィカルな解釈

左、前、右-それぞれ1,2,3。 縦線はヘビのシンボルです。
これらは分析の領域です。 背後にあるものを分析することは意味をなさないので、エリア1と2は水平線で下に区切られます。 セクターを壁で「埋める」場合、使用されるのは(ヘビの骨の場合のように)円形セクターではなく、それに刻まれた三角形であることに注意してください。
用語のメンバーシップ関数の実装
次の3つの領域があることがわかります。
左、右、前。 これらのすべての領域は、円の半分、および個別にそれぞれ1/3のセクターになります。
セクターには以下が含まれます。
1)すべてのヘビの骨(ヘビ自体を含む-尾を回る必要があります)、すなわち、その座標と数
2)壁(最大2つ、角度に共通点がある場合)
このようなセクターは、「ヘビまでの距離」、「ヘビの密度」、「壁までの距離」、「コーナーを打つ」機能の入力に供給されます。 さらに帰属の度合いが戻ってきて、彼らと一緒に何をすべきかをすでに知っています。 関数自体も複雑ではありません。
最も難しい瞬間は、そのようなセクターを形成することです。
セクターの分野と責任
サークルセクタークラスCircleSector
動作:
1)ポイントがセクターに属していることを確認する
2)セクターが常に長方形の中央にあることに留意して、長方形との交点(0〜4ポイント)を見つけます。
3)ヘビの骨と壁に従って初期化する(上記の方法を使用)
4)分を見つける。 ヘビまでの距離(言語。変数1)
5)ヘビの密度を調べる(lingu。Trans。2)
6)分を見つける。 壁までの距離(ling。Trans。3)
7)角を打つ程度を調べる
8)最大の危険度を調べる
4から7-メンバーシップ関数の実装。
8-最大4-7を探します。
フィールド:
ポイント-ヘビの骨の座標(中心)
ポイント-壁セグメントの座標(0、2、3(角度)、4)
変更を加える
ファジーAIの最初のバージョンを実装した後、
Serpent's Madnessを起動すると、いくつかの欠点があります。
危険がないとき、蛇が回っていることが明らかになりました。
セクター内で同じ脅威値を持つminmax関数は、最初のセクターを返します。 そして、最初は正しいです。 最初の-フロントセクターを作りました。 これで、必要に応じて、デフォルトでヘビが前方に移動します。
私は、ヘビが壁に垂直に乗ると、壁に衝突することに気付きました。 この場合、分析は通常どおり続行されます(すべてが正しく初期化されます)。 垂直に移動する場合、すべてのセクターは1つの壁を等しく含み、前部セクターはそれぞれ「最も近い」と思われ、その中の脅威は最小限です。 これを修正し、前のセクターを他のセクターよりわずかに長くします。 その後、メンバーシップ関数は、壁に垂直に移動すると大きな脅威を返します。 他のセクターと比較して、セクターの半径がすでに1.1倍になっているため、このバグは軽減されます。
時々、ヘビは頭を一緒に、または4つもクラッシュさせます。 すべてのセクターが2倍に増加すると、衝突の頻度が少なくなることが実験的に確立されています。 しかし、その後、ヘビはあまりにも慎重になります-彼らが回す最小限の危険から遠く離れて、演奏はそれほど面白くなりません。 それでも、ヘビは「額」と衝突することがあります。 私の意見では、これはこの種の知性の問題です。次の時点で起こりうる敵の行動の分析をせずに、現時点では近くのエリアのみを分析します。
JavaおよびAndroid SDKリスト(v2.1)
そして今、私は動作中のコードを提供します。これは、何らかの理由で、ファジーロジックを使用した人工知能に関するほとんどの記事(私が見た)でめったに行われません。 長い蛇
蛇の狂気でレベルをプレイすることで、このAIがどのように機能するかを明確に見ることができます。 記事の編集時、これは4番目のレベルです。
package com.iz.serpents.tools; import java.util.Collections; import java.util.LinkedList; import java.util.List; import android.graphics.RectF; import com.iz.serpents.model.Movement; import com.iz.serpents.model.Serpent; import com.iz.serpents.model.SerpentCollidable; public class CircleSector { public boolean hasPoint(Vector pt) { return (pt.qdist(O) < rad && pt.isOnLine(O, A)<0 && pt.isOnLine(O, B)>0); } public CircleSector(float circleRadius, int movementType, Vector aim, Vector circleCenter) { if(movementType == Movement.FORWARD) rad = circleRadius * 1.1f; else rad = circleRadius; O = circleCenter; type = movementType; _ptBones = new LinkedList<Vector>(); _ptWalls = new LinkedList<Vector>(); Vector ortAim = (Vector) aim.clone(); ortAim.normalize(); Vector vC = ortAim.mul(rad); Vector vAC = null, vCB = null; if(type == Movement.LEFT || type == Movement.FORWARD) { vAC = (Vector) vC.clone(); vAC.qrotate(30, Movement.LEFT); } if(type == Movement.FORWARD || type == Movement.RIGHT) { vCB = (Vector) vC.clone(); vCB.qrotate(30, Movement.RIGHT); } switch(type) { case Movement.LEFT: Vector lo = (Vector) ortAim.clone(); lo.leftOrtogonalRotate(); A = O.add( lo.mul(rad) ); B = O.add(vAC); break; case Movement.FORWARD: A = O.add(vAC); B = O.add(vCB); break; case Movement.RIGHT: Vector ro = (Vector) ortAim.clone(); ro.rightOrtogonalRotate(); A = O.add(vCB); B = O.add( ro.mul(rad) ); break; } possibleBonesInside = (int) ((rad*rad)/ (6f*Serpent.boneRad()*Serpent.boneRad())); } public void addBones(List<? extends Serpent> serpents, int indAI) { _indAI = indAI; for(int j = 0;j<serpents.size();j++) { Serpent s = serpents.get(j); int start = 0; if(j==indAI) start = SerpentCollidable.afterNeckInd; for(int i=start;i<s.numBones();i++) { if(hasPoint(s.bone(i))) _ptBones.add(s.bone(i)); } } } public int getNumBones() { return _ptBones.size(); } public void addWalls(RectF walls) { List<Vector> walls_points = new LinkedList<Vector>(); walls_points.add(Vector.create(walls.left, walls.top)); walls_points.add(Vector.create(walls.right, walls.top)); walls_points.add(Vector.create(walls.right, walls.bottom)); walls_points.add(Vector.create(walls.left, walls.bottom)); walls_points.add(Vector.create(walls.left, walls.top)); for(int i=0;i<walls_points.size()-1;i++) { Vector common;
結果
以下は、AIデバッグモードでの
Serpent's Madnessのスクリーンショットです。
白はセクターの境界を示します-三角形(壁用、骨用-簡単に想像できます)。

セクターの骨は黄色で強調表示されます。

そして赤で-セクターの壁。

時には衝突します。 上で書いたように、これはセクターを増やすことで排除されます。 しかし、ゲームの一環として、無敵のAIは必要ありませんでした。

しかし、全体的に、AIは素晴らしいです。

これはAndroidプラットフォーム向けの最初の開発であり、ファジーロジックを備えた最初のAIです。提案やコメントがあれば、いつでも聞く準備ができています。 ご清聴ありがとうございました。 ゲームに興味がある場合は、Androidマーケットからダウンロードできます。
蛇の狂気