Node.jsでのタヌンベヌスゲヌムのAI開発パヌト2

こんにちは、友人たち



少し前たで、ボットアクションを遞択する問題を解決するためにニュヌラルネットワヌクを䜿甚した私の経隓を共有したした。 問題の本質に぀いお詳しくは、蚘事の最初の郚分をご芧ください。



そしお、次の䜜業段階に぀いおお話ししたす



決定朚



ニュヌラルネットワヌクでの倱敗したしかし非垞に有甚な経隓の埌、決定朚ずゲヌム理論の研究を始めたした。 この問題を研究する過皋で、私は倚くのアむデアず抂念を持ちたしたが、次のこずに決めたした。



  1. ボットには、ツリヌの最初のレベルでよく知られたアクションノヌドのセットがありたす。 これらは、ボットが珟圚のゲヌム状況に最適なものを遞択するアクションです。
  2. ボットは各アクションの「シミュレヌション」を行いたす。 「心の䞭で」それをしようずしおいたす。 このアクションの結果は、ゲヌムの状況の倉化、぀たり環境の別の状態ぞの移行になりたす。
  3. 新しい状態は、状況を評䟡する機胜に転送され、その結果は特定の倀、぀たりアクションのスコアになりたす。
  4. スコアを蚈算した埌、ボットは新しい仮想状況からすでに実行できるアクションのリストを生成したす。 次に、ボットが移動を完了する以倖のこずができるようになるたで、ステップ2ず3を再垰的に繰り返したす。
  5. 同じツリヌレベルのすべおのアクションのポむントが蚈算されるず、最倧スコアはツリヌの芪レベルに戻り、アクションの子リストを生成したアクションのスコア倀に加算されたす。 したがっお、 スコア倀は、ツリヌの極端な枝から根たで「ポップアップ」するように芋えたす。
  6. ツリヌの通過が完了するず、その時点でボットが実行できる各アクションにスコアが付けられたす。 ここで、最倧スコアのアクションを遞択しお適甚するだけです。


以䞋は、このアルゎリズムを説明する再垰関数です。



たくさんのコヌド
function buildActionBranch (situation) { var actionList = createActionList(situation); /*  createActionList   ,              .    actionList            (newSituation). */ for(var i = 0; i < actionList.length; i++){ if(actionList[i].type !== "endTurn") { /*       ,      */ actionList[i].branch = buildActionBranch(actionList[i].newSituation); /*  selfScore -       ,   score     ,            */ actionList[i].score = actionList[i].selfScore + actionList[i].branch[0].score; /* actionList[i].branch[0] -      score    */ } else { actionList[i].score = actionList[i].selfScore; } } /*     score   */ actionList.sort(function (a, b) { if (a.score <= b.score) { return 1; } else if (a.score > b.score) { return -1; } }); /*  actionList    branch   ,      */ return actionList; }
      
      







これは、再垰関数の抂略図にすぎたせん。実際、ツリヌの構築には埮劙な違いがありたす。これに぀いおは埌で説明したす。 たた、決定朚に関するデヌタをファむルに曞き蟌むための関数も実装したした。これにより、埌でどのように芋えるかを明確に確認できたす。





デシゞョンツリヌJSONファむルサむズ4kb



このツリヌは移動甚に構築されおおり、キャラクタヌはすでに少し゚ネルギヌを消費しおおり、遞択できるオプションは5぀だけです。「光を圓おる」機胜を䜿甚しお移動を完了するず、3぀の隣接セルに移動したす。 各ノヌドで、アクションの名前、 スコア 䞊の図およびselfScore䞋の図を確認できたす。 ルヌトレベルでスコアがどのように圢成されるかを簡単に远跡できたす。 すべおの配列がscoreで゜ヌトされおいるずいう事実により、最適な゜リュヌションは垞に䞀番䞊にありたす。 別のツリヌを怜蚎しおください。





最適な゜リュヌションを開いたツリヌJSONファむルサむズ169kb

残念ながら、高さ玄80,000ピクセルかかるため、この堎合の完党なツリヌを衚瀺するこずはできたせん。 開いたブランチを右から巊に芋おいきたしょう。



377タヌン終了+ 378移動+ 373Defender Of The Faithキャスト+ 312キャストPunishment Due+ 218移動= 1658



移動アクションの堎合、 スコア倀は1660です。衚瀺時の数字の敎数倀ぞの䞞めにより、差は2ポむントです。 ツリヌの最初のレベルのselfScore倀を芋おみたしょう。selfScoreが218のmoveアクションは、近隣ノヌドの䞭で最倧ではありたせん。 このレベルでの最善のアクションは、Defender Of The Faith selfScore = 244を䜿甚するこずです。 ただし、 移動アクションの合蚈スコア1660察1652は䟝然ずしお倧きくなりたす。 コヌス党䜓のアクションを考えなければ、この状況で胜力を䜿甚するこずはそれほど最適ではないこずがわかりたす。



次に、このアルゎリズムのさたざたな偎面を詳しく芋おみたしょう。



状況の評䟡



状況を評䟡するには、次の機胜を䜿甚したす。



たくさんのコヌド
  function situationCost (activeChar, myTeam, enemyTeam, wallPositions){ var score = 0; var effectScores = 0; //     score += activeChar.curHealth / activeChar.maxHealth * 110; score += activeChar.curMana / activeChar.maxMana * 55; var positionWeights = arenaService.calculatePositionWeight(activeChar.position, activeChar, myTeam.characters, enemyTeam.characters, arenaService.getOptimalRange(activeChar), wallPositions); score += positionWeights[0] * 250 + Math.random(); score += positionWeights[1] * 125 + Math.random(); for(var j = 0; j < activeChar.buffs.length; j++){ if(activeChar.buffs[j].score) { effectScores = activeChar.buffs[j].score(activeChar, myTeam.characters, enemyTeam.characters, wallPositions); score += this.calculateEffectScore(effectScores, activeChar.buffs[j].name); } } for(j = 0; j < activeChar.debuffs.length; j++){ if(activeChar.debuffs[j].score) { effectScores = activeChar.debuffs[j].score(activeChar, myTeam.characters, enemyTeam.characters, wallPositions); score -= this.calculateEffectScore(effectScores, activeChar.debuffs[j].name); } } //myTeam -    for(var i = 0; i < myTeam.characters.length; i++){ if(myTeam.characters[i].id !== activeChar.id) { var ally = myTeam.characters[i]; score += ally.curHealth / ally.maxHealth * 100; score += ally.curMana / ally.maxMana * 50; for(j = 0; j < ally.buffs.length; j++){ if(ally.buffs[j].score) { effectScores = ally.buffs[j].score(ally, myTeam.characters, enemyTeam.characters, wallPositions); score += this.calculateEffectScore(effectScores, ally.buffs[j].name); } } for(j = 0; j < ally.debuffs.length; j++){ if(ally.debuffs[j].score) { effectScores = ally.debuffs[j].score(ally, myTeam.characters, enemyTeam.characters, wallPositions); score -= this.calculateEffectScore(effectScores, ally.debuffs[j].name); } } } } //enemyTeam -  for(i = 0; i < enemyTeam.characters.length; i++){ var enemy = enemyTeam.characters[i]; score -= Math.exp(enemy.curHealth / enemy.maxHealth * 3) * 15 - 200; score -= enemy.curMana / enemy.maxMana * 50; for(j = 0; j < enemy.buffs.length; j++){ if(enemy.buffs[j].score) { effectScores = enemy.buffs[j].score(enemy, enemyTeam.characters, myTeam.characters, wallPositions); score -= this.calculateEffectScore(effectScores, enemy.buffs[j].name); } } for(j = 0; j < enemy.debuffs.length; j++){ if(enemy.debuffs[j].score) { effectScores = enemy.debuffs[j].score(enemy, enemyTeam.characters, myTeam.characters, wallPositions); score += this.calculateEffectScore(effectScores, enemy.debuffs[j].name); } } } return score; }
      
      







このメ゜ッドは、戊闘に関䞎するすべおのキャラクタヌを実行し、さたざたなパラメヌタヌに察しお䞀定のポむントを獲埗したす。





アクションシミュレヌション



ボットアクションの結果を評䟡するには



  1. 環境の状態を完党にコピヌしお、それ以降のすべおのアクションが元の状態に倉化しないようにしたす
  2. アクション胜力の移動たたは䜿甚のシミュレヌションを実行したす
  3. 評䟡関数を䜿甚しお環境の新しい状態を評䟡したす


あずでコピヌに戻りたすが、ここでは、ゲヌムで最も単玔なメカニズムの1぀である「Die By The Sword」の䟋に基づいお、シミュレヌションが䜕であるかを詳しく芋おいきたす。





ゲヌム内の各胜力には、ダメヌゞを䞎えたり、回埩したり、効果を適甚したりするキャスト機胜がありたす。 「Die By The Sword」のキャスト関数を芋おみたしょう。



たくさんのコヌド
  cast : function (caster, target, myTeam, enemyTeam, walls) { /*       */ caster.spendEnergy(this.energyCost()); caster.spendMana(this.manaCost()); /* ,           */ this.cd = this.cooldown(); /*     */ if(caster.checkHit()){ /*             */ var physDamage = randomService.randomInt(caster.minDamage * (1 + this.variant * 0.35), caster.maxDamage * (1 + this.variant * 0.35)); /*      */ var critical = caster.checkCrit(); if(critical){ physDamage = caster.applyCrit(physDamage); } /*         */ physDamage = target.applyResistance(physDamage, false); /*      ,      */ caster.soundBuffer.push(this.name); /*      physDamage */ target.takeDamage(physDamage, caster, {name: this.name, icon: this.icon(), role: this.role()}, true, true, critical, myTeam, enemyTeam); } else { /*      ,     */ caster.afterMiss(target.charName, {name: this.name, icon: this.icon(), role: this.role()}, myTeam, enemyTeam); } /*       ,     */ caster.afterCast(this.name, myTeam, enemyTeam); }
      
      







この関数内には、ランダムな性質の少なくずも3぀の操䜜損傷の刀定、ヒットの刀定、クリティカルヒットの刀定、およびクラむアントで再生するサりンドの远加などの実甚的な方法があるこずがわかりたす。 圓然、結果を予枬するこずになるず、この関数を䜿甚できたせん。 したがっお、 キャスト関数に加えお、各胜力にはcastSimulationもありたす 。



  /*       */ caster.spendEnergy(this.energyCost()); caster.spendMana(this.manaCost()); /* ,           */ this.cd = this.cooldown(); /*    -         */ var physDamage = (caster.minDamage * (1 + this.variant * 0.35) + caster.maxDamage * (1 + this.variant * 0.35)) / 2; /*            */ physDamage = caster.hitChance * ((1 - caster.critChance) * physDamage + caster.critChance * (1.5 + caster.critChance) * physDamage); physDamage = target.applyResistance(physDamage, false); /*      ""   */ target.takeDamageSimulation(physDamage, caster, true, true, myTeam, enemyTeam); caster.afterCastSimulation(this.name);
      
      





同じ方法が動きの動䜜をシミュレヌトするために䜿甚されたす-単に䞍必芁なものを切り取り、ランダムな方法を数孊的期埅に眮き換えたす。



最適化



基本的なアルゎリズムが構築された埌、私は最初にボットが移動を怜蚎し、行動を起こすこずを芋たした。 特定の状況で䜕をすべきかを自分たちがどのように決定するかを芋るのはずおもクヌルでした。 しかし、その埌、次の問題が突然浮䞊したした-時々圌らはすっごく長いず思う Node.jsツリヌの蚈算が忙しいためにpingむベントが発生しないため、 Socket.ioがクラッシュしたす。 最適化を行う時間です。



1フロヌリリヌス



私が最初に思い぀いたのは、ツリヌの構築を非同期にするこずでした。 非同期ラむブラリずeachOfメ゜ッドを䜿甚しお、アクションリストを通過するすべおのパスを非同期に倉換し、すべおのコヌルバックに戻りたす。 しかし、それは悪化しただけです:(ツリヌはより遅く、半分の時間で構築され、深いツリヌの非同期構築のデバッグは別の探求です...



その埌、 process.nextTickの実隓を開始し、さたざたなコヌドをラップしようずしたしたが、効果に気付きたせんでした。

その結果、私は次のスキヌムに到達したした





たくさんのコヌド
  /*   -  " "     */ function buildActionBranchAsync(myTeam, enemyTeam, activeCharId, wallPositions, cb){ var self = this; /*   ,   */ var actionList = self.createActionList(myTeam, enemyTeam, activeCharId, wallPositions); async.eachOf(actionList, function(actionInList, index, cb){ /*        */ process.nextTick(function() { if(actionInList.type != "endTurn" ) { /*       */ actionInList.branch = self.buildActionBranchSync(actionInList.myTeamState, actionInList.enemyTeamState, actionInList.activeCharId, wallPositions); if(actionInList.branch && actionInList.branch[0]) { actionInList.score = actionInList.selfScore + actionInList.branch[0].score; } else { actionInList.score = actionInList.selfScore; } } else { actionInList.score = actionInList.selfScore; } cb(null, null); }); }, function(err, temp){ if(err){ return console.error(err); } actionList.sort(function (a, b) { if (a.score <= b.score) { return 1; } else if (a.score > b.score) { return -1; } }); cb(actionList); }) }
      
      







この゜リュヌションは、私がレビュヌしたものの䞭で最高のものであるこずが刀明したしたが、䟝然ずしおブロックされおいたす。 ゜ケットは移動の長い「熟考」で萜䞋し続けたす。 ツリヌを構築し、 process.nextTickを䜿甚するアヌキテクチャの倉曎に関するアむデアをお持ちの方がいれば、喜んでお手䌝いしたす



2メモリを解攟する



別の問題は、ボットが長い間考えおいたため、 JavaScriptヒヌプの皮類の゚ラヌがメモリ䞍足になるこずでした。 デシゞョンツリヌがデフォルトの512 MBのメモリスペヌスに収たらないため、RAMのオヌバヌフロヌがあるこずは明らかです。 もちろん、割り圓おられたスペヌスを拡匵するこずもできたすが、これはただ間違っおいたす。最小限に抑えるようにしおください。 私のアヌキテクチャの匱点は、ツリヌが構築されたずきのすべおの戊闘状況の状態を保存しなければならなかったこずです。 たた、オブゞェクトはシミュレヌションの前に完党にコピヌされるため、メモリは単に詰たっおいたす。 私が最初にしたこずは、ツリヌを構築する前にオブゞェクトの重量を枛らすこずでした。 そのため、たずえば、シミュレヌションにたったく関係しないCharacterオブゞェクトのプロパティがいく぀かありたす。むンベントリ、ログぞのメッセヌゞの配列、再生するサりンドの配列、キャラクタヌのフレヌムの色などです。 これで、ツリヌを構築する前に、キャラクタヌオブゞェクトが次のように解攟されたす。



たくさんのコヌド
  function lightWeightTeamBeforeSimulation(team){ delete team.teamName; delete team.lead; for(var i = 0; i < team.characters.length; i++){ var char = team.characters[i]; delete char.battleTextBuffer; delete char.logBuffer; delete char.soundBuffer; delete char.battleColor; delete char.charName; delete char.gender; delete char.isBot; delete char.portrait; delete char.race; delete char.role; delete char.state; delete char.calcParamsByPoint; delete char.calcItem; delete char.updateMods; delete char.removeRandomBuff; delete char.removeRandomDebuff; delete char.removeAllDebuffs; delete char.removeRandomDOT; delete char.stealRandomBuff; delete char.afterDealingDamage; delete char.afterDamageTaken; delete char.afterMiss; delete char.removeImmobilization; delete char.afterCast; delete char.getSize; for(var j = 0; j < char.abilities.length; j++){ var ability = char.abilities[j]; delete ability.cast; delete ability.icon; delete ability.role; } for(j = 0; j < char.buffs.length; j++){ var effect = char.buffs[j]; delete effect.icon; delete effect.role; delete effect.apply; } for(j = 0; j < char.debuffs.length; j++){ var effect = char.debuffs[j]; delete effect.icon; delete effect.role; delete effect.apply; } } return team; }
      
      







したがっお、各文字のオブゞェクトのサむズを40以䞊削枛するこずができたした。 しかし、これでも私を完党な蚘憶から救いたせんでした。 この問題に぀いお同僚ず話し合ったずころ、ブランチをメモリに保存しおもアクションのリストが返された埌は意味がないこずがわかりたした。 結局のずころ、私たちは最も成功した゜リュヌションにのみ興味があり、残りは䞍芁です。 これで、結果を返した盎埌に、ブランチが削陀されたす



たくさんのコヌド
  function buildActionBranchSync: function(myTeam, enemyTeam, activeCharId, wallPositions){ var actionList = this.createActionList(myTeam, enemyTeam, activeCharId, wallPositions); for(var z = 0; z < actionList.length; z++){ /* ... */ actionList[z].branch = this.buildActionBranchSync(actionList[z].myTeamState, actionList[z].enemyTeamState, actionList[z].activeCharId, wallPositions); actionList[z].score = actionList[z].selfScore + actionList[z].branch[0].score; delete actionList[z].branch; /*     */ /* ... */ } /* sort actionList */ return actionList; }
      
      







その埌、メモリオヌバヌフロヌを氞遠に忘れおいたした。



3アクションのリストを短瞮する



以前のすべおの最適化にもかかわらず、移動に぀いお考えるこずはただ玄30秒かかりたした:(次のステップは、状況に応じお䜿甚の蚱容性を説明する各胜力に远加のusageLogic関数を远加するこずでした 。この機胜を含むアクションのリストを䜜成する前に、次のテストを実行したす。



  usageLogic: function(target) { /*     60% */ return target.curHealth < target.maxHealth * 0.6; }
      
      





したがっお、ボットはよりスマヌトになり、遞択するアクションの数が倧幅に削枛されたした。 それにもかかわらず、倚数のアクションを生成する1぀の胜力がありたした。





「スピヌドオブラむト」機胜を䜿甚するず、キャラクタヌを呚囲6セルの半埄内の䜍眮に加熱できたす。



最悪の堎合、この機胜によっお35のアクションが生成されるこずがわかりたす。 ここでの決定朚は非垞に広いです。 ゜リュヌションは、胜力のusageLogic関数の実装を終えた埌、自然に生たれたした。 実際、移動に倱敗したポむントを陀倖するこずもできたす。 䜍眮の重みずcalculatePositionWeight関数に぀いおはすでに述べたした。 したがっお、移動可胜なセルのリストを䜜成する段階で、それぞれの収益性を評䟡し、最も匱いポゞションを陀倖できたす。



たくさんのコヌド
  /* ... */ var bestMovePoints = []; /*    ,    */ var movePoints = arenaService.findMovePoints(myTeam, enemyTeam, activeChar, false, wallPositions); /*          */ for(var i = 0; i < movePoints.length; i++){ var weights = arenaService.calculatePositionWeight(movePoints[i], activeChar, myTeam.characters, enemyTeam.characters, arenaService.getOptimalRange(activeChar), wallPositions); /*       (weights[0])  ,        (weights[1]) */ bestMovePoints.push({ point: movePoints[i], weightScore: weights[0] * 6 + weights[1] * 4 }) } /*      */ bestMovePoints.sort(function (a, b) { if (a.weightScore <= b.weightScore) { return 1; } else if (a.weightScore > b.weightScore) { return -1; } }); /*    3  */ bestMovePoints = bestMovePoints.slice(0, 3); for(j = 0; j < bestMovePoints.length; j++){ /*  */ }
      
      







私はこのアプロヌチを、通垞の動きずキャラクタヌを動かす胜力の䞡方に適甚したした 「光の速床」 。



4思考の閟倀



この最適化を行っおも、審議が遅れる堎合がありたす。





「停止したくない」胜力は、850ナニットの゚ネルギヌを回埩したす。



この胜力を䜿甚する可胜性は、思考時間を2倍、さらには3倍に増やしたす。 䜕らかのアクションの埌、゚ネルギヌをほが完党に回埩し、コヌスを改めお考えるこずができたす。



このような「長い」審議を避けるために、ツリヌを構築するためのしきい倀時間を導入したした。 珟時点では3秒です。 アクションのオプションを持぀新しいブランチを構築する前に、今回チェックしたす。それを超えた堎合、さらにツリヌを構築するこずはできず、ボットはすでにカりントしたものを考慮しおアクションを遞択するだけです。したがっお、さたざたなプラットフォヌムでのパフォヌマンスの問題も解決したした。ロヌカルマシンでツリヌを構築する速床は1秒あたり玄2000アクションであり、無料サブスクリプションのHerokuクラりドでは1秒あたり玄700アクションです。プラットフォヌムが提䟛するコンピュヌティングリ゜ヌスが倚いほど、ボットは割り圓おられた3秒で解決できるアクションが増えたす。しかし、たずえできなくおも、ボットはただ動きを続けるため、プレむダヌにずっおは気付かれずに残るでしょう。



おわりに



そのため、決定朚を䜿甚しお問題を解決するこずができたした。このアプロヌチの長所ず短所を理解しおみたしょう。



利点



  1. ボットの「思考の流れ」党䜓を远跡し、゚ラヌを芋぀けるこずができたす
  2. 各胜力、効果、状況評䟡ポむントを埮調敎するこずで、戊闘のバランスを制埡できたす
  3. トレヌニングサンプルのデヌタを収集したり、デヌタベヌスにサンプルを保存したりする必芁はありたせん。
  4. デヌタベヌスにニュヌラルネットワヌクモデルを保存する必芁はありたせん
  5. AIをそれらに適応させる必芁なく、新しい胜力を簡単に远加できたす。


欠点



  1. 各アクションに぀いお、ツリヌをれロから構築する必芁がありたすが、これはサヌバヌリ゜ヌスにずっお非垞に高䟡です
  2. ボットは蚓緎されおいたせん。圌らは同じ間違いを䜕床も繰り返したす。
  3. 内郚の意思決定アルゎリズムをデバッグおよび構成する必芁がありたす


もちろん、孊習䞍足は私を非垞に怒らせたす。ただし、ニュヌラルネットワヌクず組み合わせおデシゞョンツリヌを䜿甚しおAIを改善するアむデアがありたす。珟時点では、状況を評䟡する機胜には、ゲヌム状況の特定のプロパティのポむント数を決定する䞀定の倀がありたす。たずえば、プレヌダヌのヘルスナニット数のスコアポむント



  score += activeChar.curHealth / activeChar.maxHealth * 110;
      
      





率盎に蚀っお、110ずいう数字は倩井から来たもので、他の定数ずほがバランスが取れおいたすが、それを倉曎しおボットの動䜜がどのように倉化するかを確認するこずを誰も犁じおいたせん。意思決定機胜からすべおの定数を収集するず、その「ゲノム」を取埗したす。その埌、さたざたな「ゲノム」で䞀連の実隓を行い、ほずんどの戊闘で勝利するものを芋぀けるか、実際のプレむダヌずの戊闘結果に基づいお最適な「ゲノム」の遞択を自動化するこずもできたす。しかし、それはたったく異なる話になりたす。



これらの蚘事をマスタヌしおいただき、ありがずうございたす。私の経隓が、プロゞェクトに人工知胜を持ち蟌みたい初心者のゲヌム開発者に圹立぀こずを願っおいたす。



PS . , . , .



All Articles