スポンサーリンク

果心異境をHPダメージで攻略 part5(AIのシミュレーション)

どうも、グミタブログです。

中途半端な状態で止まっていた「果心異境をHPダメージで攻略」シリーズの続き記事となります。
(続きですが、過去記事を読まなくてもわかる内容となっているのでご安心をw)

 

参考過去記事:果心異境をHPダメージで攻略 part1part2part3part4

 

今回は、生成AIに果心砦攻略のシミュレーションのコードを生成してもらいました。
それを使うことで、「攻略にかかる時間の予測」が可能になりました。
(攻略法方法は過去と同様に、茶人回復無し、家康・伊達藤次郎の回復を使った"攻撃後即負傷"のサイクルを繰り返す攻略となっています)

 

使用したAIは、Geminiの有料版(Pro)です。
(無料でも使えますが、返答結果が変わってくるかもしれません)

 

作られたコードを使うと、こんな感じでシミュレーションができます。

 

100回試行した時の結果一覧とヒストグラムも作れます。

 

 

 

 

AIへのプロンプトと生成されたコード

一例として、私からのAIへの指示内容(プロンプト)と、AIから返ってきたコードを紹介します。

 

プロンプト

とあるゲームの実行結果・途中経過のシミュレーションをスプレッドシート上に書き出すプログラムコードを作って欲しい。
コードはGoogle Apps Script (GAS)で使用する言語で。

ゲームの内容は以下の通り。

【ゲーム概要】
砦にいる敵を攻撃し撃退するゲーム。
敵全員を負傷させたらゲームクリア。

 

【敵について】
敵は砦に4体いる。
敵のHPの初期値は150。最大値も150。
敵のHPは62.4秒毎に1回復する。回復が始まるタイミングは、敵のHPが149以下になった時。
敵のHPが1未満になると敵は負傷し、砦から消滅する。

 

【攻撃について】
こちらには攻撃部隊が4つある。
それぞれをA,B,C,D部隊とする。

全部隊共通の仕様
・部隊のデッキ配置から出陣までに要する時間は50秒。
・出陣してから2分後に砦の敵に攻撃する。攻撃後はデッキアウトし、再配置するには部隊毎に定められた待機時間が必要となる。
・攻撃により、砦の敵に毎回一定の固定ダメージを与える。
・部隊毎に定められた待機時間終了後、即座にデッキに配置し出陣させる。
・攻撃部隊は敵を全滅させるまで、デッキ配置→出陣→待機 を繰り返していく。
・デッキ配置はA,C部隊から始める。

部隊毎に異なる仕様
・待機時間は、A部隊とB部隊が378秒。C部隊が1076秒。D部隊が1304秒。
・A部隊とB部隊は同時にデッキ配置・出陣することはできない。Aの待機時間中にBを、Bの待機時間中にAを配置・出陣させる運用となる。
・C部隊とD部隊も同様に、同時にデッキ配置・出陣することはできない。Cの待機時間中にDを、Dの待機時間中にCを配置・出陣させる運用となる。
・B,C,D部隊は1回の攻撃で、敵全員に5のダメージを与える。
・A部隊は、砦の敵のランダム(等確率)な1体に20のダメージを与える。これに加え、敵のランダムな3体に5のダメージを与える。この5のダメージは同じ敵には重複しないが、20と5は重複可能。この抽選は攻撃毎に行われる。5のダメージは、敵が3体以下になると敵全員への5のダメージとなる。

クリア(砦の敵の全滅)までにかかる時間を秒単位で計算する。
秒の隣に〇分〇秒も併記する。
計算の経過について、敵の回復ログは表示させない。

プログラムには「100回試行するモード」も加えて、試行結果はグラフでも表示する。
敵のHP回復間隔(62.4秒)、攻撃部隊のデッキ配置にかかる時間(50秒)、AからD部隊の待機時間は、変数として書き換えられるようにする。

 

プロンプトについての補足

先ほどのプロンプトに盛り込んだ条件について、補足します。

敵1部隊を敵1体と表現しています(わかり易くするため)。
敵のHPは150、回復速度は極武将の速度でみています(実際の攻略から、なんとなく予測される値です)。
こちらのスキル(示現流、天啓)はすべて発動する想定で計算してます。
デッキ配置~出陣にかかる手作業の時間は50秒としています。配置作業自体は50秒もかからないのですが、HP回復後にすぐに配置できるとは限らない(別の事をしてたりして放置する事もある)ので、それを考慮しやや長めの時間に設定しています。

 

私が異境攻略に使う攻撃部隊(固定ダメージ部隊)は以下の4部隊です。
内訳としては、回復家康(覇1263)の部隊が2つ、伊達藤次郎部隊が2つ。
家康部隊の1つに示現流の東郷さんがいます。

A部隊(東郷入り)

ダメージスキル:示現流1,天啓ノ神撃3
(この家康は3マネで育てているので天啓をつけていません)

 

B部隊

ダメージスキル:天啓4

 

C部隊(藤次郎TR2)

ダメージスキル:天啓4

 

D部隊

ダメージスキル:天啓4

 

以上の内容をプロンプトに記述しました。

 

AIが生成したコード

同じAIに同じプロンプトを出しても、常に同じ結果が返ってくるわけではないのでご注意ください。

コードはスプレッドシートのApps Script用のものです。
Apps Scriptはスプレッドシートの画面上部の拡張機能から無料で始められます。(使い方や貼り付け方も、わからなければAIに聞く方が早いですね)
エクセルのVBA等を使いたい方は、AIに書き換えしてもらうとよいと思います。

 

なお、以下↓のコードは先ほどのプロンプトから一発で完成したわけではなく、少しやり取りをして修正した末にできたものです。

 

/**
 * — シミュレーションの設定 —
 * ここにある数字を書き換えるだけで、色々なパターンを試せるよ!
 */
const CONFIG = {
  deckPlacementTime: 50, // デッキ配置にかかる時間(秒)
  timeToAttack: 120,     // 出陣から攻撃までに要する時間(秒)
  cooldownA: 378,        // A部隊の待機時間(秒)
  cooldownB: 378,        // B部隊の待機時間(秒)
  cooldownC: 1076,       // C部隊の待機時間(秒)
  cooldownD: 1304,       // D部隊の待機時間(秒)
  healInterval: 62.4     // 敵のHP回復間隔(秒)
};
/**
 * スプレッドシートを開いた時に専用メニューを追加する
 */
function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('シミュレーション’)
    .addItem('1回試行(経過書き出し)’, 'runSingleSimulation’)
    .addItem('100回試行(結果&グラフ書き出し)’, 'runMultipleSimulations’)
    .addToUi();
}
/**
 * 秒数を「〇分〇秒」の形式に変換する補助関数
 */
function formatTime(seconds) {
  const m = Math.floor(seconds / 60);
  const s = Math.floor(seconds % 60);
  return `${m}分${s}秒`;
}
/**
 * 1回だけシミュレーションを実行して、ログをシートに出力する
 */
function runSingleSimulation() {
  const result = simulate(true);
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('1回試行ログ’);
  if (!sheet) {
    sheet = ss.insertSheet('1回試行ログ’);
  } else {
    sheet.clear();
  }
  const headers = ['Time(秒)’, 'Time(分秒)’, 'イベント内容’, '敵1 HP’, '敵2 HP’, '敵3 HP’, '敵4 HP’];
  sheet.appendRow(headers);
  if (result.logs.length > 0) {
    sheet.getRange(2, 1, result.logs.length, result.logs[0].length).setValues(result.logs);
  }
  SpreadsheetApp.getUi().alert(`シミュレーション完了!クリアタイムは ${result.clearTime.toFixed(1)}秒(${formatTime(result.clearTime)}) だったよ。`);
}
/**
 * 100回シミュレーションを実行して、結果とヒストグラムを出力する
 */
function runMultipleSimulations() {
  const N = 100;
  let results = [];
  for (let i = 0; i < N; i++) {
    let r = simulate(false);
    results.push([i + 1, Number(r.clearTime.toFixed(1)), formatTime(r.clearTime)]);
  }
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('100回試行結果’);
  if (!sheet) {
    sheet = ss.insertSheet('100回試行結果’);
  } else {
    sheet.clear();
    const charts = sheet.getCharts();
    for (let c of charts) {
      sheet.removeChart(c);
    }
  }
  sheet.appendRow(['試行回数’, 'クリアタイム(秒)’, 'クリアタイム(分秒)’]);
  sheet.getRange(2, 1, results.length, 3).setValues(results);
  const range = sheet.getRange(1, 2, results.length + 1, 1);
  const chart = sheet.newChart()
    .asHistogramChart()
    .addRange(range)
    .setPosition(5, 4, 0, 0)
    .setOption('title’, 'クリアタイムの分布 (100回試行)’)
    .setOption('legend’, {position: 'none’})
    .setOption('hAxis’, {title: 'クリアタイム(秒)’})
    .setOption('vAxis’, {title: '度数’})
    .build();
  sheet.insertChart(chart);
  SpreadsheetApp.getUi().alert('100回試行が完了したよ!シートを確認してみてね。’);
}
/**
 * シミュレーション本体
 * @param {boolean} logging – ログを記録するかどうか
 */
function simulate(logging) {
  // CONFIGからサイクル時間(配置 + 攻撃までの時間)を計算
  const cycleTime = CONFIG.deckPlacementTime + CONFIG.timeToAttack;
  let enemies = [
    { id: 1, hp: 150, isAlive: true, nextHeal: null },
    { id: 2, hp: 150, isAlive: true, nextHeal: null },
    { id: 3, hp: 150, isAlive: true, nextHeal: null },
    { id: 4, hp: 150, isAlive: true, nextHeal: null }
  ];
  let events = [];
  // 初期攻撃イベントをスケジュール
  // BとDは、それぞれAとCが攻撃してデッキアウトした直後に配置・出陣させるため、2サイクル分(cycleTime * 2)の時間を設定
  events.push({ time: cycleTime, type: 'attack’, unit: 'A’ });
  events.push({ time: cycleTime * 2, type: 'attack’, unit: 'B’ });
  events.push({ time: cycleTime, type: 'attack’, unit: 'C’ });
  events.push({ time: cycleTime * 2, type: 'attack’, unit: 'D’ });
  let logs = [];
  if (logging) {
    logs.push([0, formatTime(0), “シミュレーション開始", 150, 150, 150, 150]);
  }
  let currentTime = 0;
  while(enemies.some(e => e.isAlive)) {
    events.sort((a, b) => a.time – b.time);
    let ev = events.shift();
    if (!ev) break;
    currentTime = ev.time;
    let eventMessage = “";
    if (ev.type === 'attack’) {
      let aliveEnemies = enemies.filter(e => e.isAlive);
      if (aliveEnemies.length === 0) break;
      eventMessage = `${ev.unit}部隊の攻撃`;
      let damages = {1:0, 2:0, 3:0, 4:0};
      if (ev.unit === 'A’) {
        let target20 = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
        damages[target20.id] += 20;
        let shuffled = aliveEnemies.slice();
        for (let i = shuffled.length – 1; i > 0; i–) {
          let j = Math.floor(Math.random() * (i + 1));
          [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; // 要素を入れ替え
        }
        let targets5 = shuffled.slice(0, 3);
        targets5.forEach(t => {
          damages[t.id] += 5;
        });
      } else {
        aliveEnemies.forEach(t => {
          damages[t.id] += 5;
        });
      }
      aliveEnemies.forEach(e => {
        if (damages[e.id] > 0) {
          applyDamage(e, damages[e.id], currentTime, events);
        }
      });
      enemies.forEach(e => {
        if (e.isAlive && e.hp <= 0) {
          e.hp = 0;
          e.isAlive = false;
          e.nextHeal = null;
          eventMessage += ` / 敵${e.id}が消滅`;
        }
      });
      // 次回の攻撃をスケジュール (サイクル時間 + 各部隊の待機時間)
      let nextAttackTime = 0;
      if (ev.unit === 'A’) nextAttackTime = currentTime + cycleTime + CONFIG.cooldownA;
      if (ev.unit === 'B’) nextAttackTime = currentTime + cycleTime + CONFIG.cooldownB;
      if (ev.unit === 'C’) nextAttackTime = currentTime + cycleTime + CONFIG.cooldownC;
      if (ev.unit === 'D’) nextAttackTime = currentTime + cycleTime + CONFIG.cooldownD;
      events.push({ time: nextAttackTime, type: 'attack’, unit: ev.unit });
    } else if (ev.type === 'heal’) {
      let enemy = enemies.find(e => e.id === ev.enemyId);
      if (enemy && enemy.isAlive && enemy.hp > 0 && enemy.hp < 150) {
        if (Math.abs(enemy.nextHeal – currentTime) < 0.001) {
          enemy.hp += 1;
          if (enemy.hp < 150) {
            enemy.nextHeal = currentTime + CONFIG.healInterval;
            events.push({ time: enemy.nextHeal, type: 'heal’, enemyId: enemy.id });
          } else {
            enemy.nextHeal = null;
          }
        }
      }
    }
    if (logging && eventMessage !== “") {
      logs.push([currentTime.toFixed(1), formatTime(currentTime), eventMessage, enemies[0].hp, enemies[1].hp, enemies[2].hp, enemies[3].hp]);
    }
  }
  if (logging) {
    logs.push([currentTime.toFixed(1), formatTime(currentTime), “全敵消滅・ゲームクリア", enemies[0].hp, enemies[1].hp, enemies[2].hp, enemies[3].hp]);
  }
  return { clearTime: currentTime, logs: logs };
}
/**
 * ダメージ処理と回復のスケジューリングを行う関数
 */
function applyDamage(enemy, amount, currentTime, events) {
  if (!enemy.isAlive) return;
  let wasMaxHp = (enemy.hp === 150);
  enemy.hp -= amount;
  if (wasMaxHp && enemy.hp < 150 && enemy.hp > 0) {
    enemy.nextHeal = currentTime + CONFIG.healInterval;
    events.push({ time: enemy.nextHeal, type: 'heal’, enemyId: enemy.id });
  }
}

 

 

上記のプログラムで計算したところ、敵部隊4つの砦16~19なら、だいたい150分前後で攻略可能という結果になりました。

 

 

 

 

条件を変えることで、敵が3部隊の砦の攻略や「気丈な決意」登場後の砦の攻略、こちらの攻撃部隊を1つ増やした場合や伊達藤次郎のTRを上げて待機時間を短くした場合、ビビを部隊に加えた場合、なども計算できます。
大がかりな変更があれば、AIに丸投げする方が早いですね。

 

まとめ

以上のように、条件を記述してAIに投げるだけで、自分でやるには躊躇するような手間のかかるシミュレーションができます。

今まで計算しづらかったIXAのあれこれも、色々試してみてはいかがでしょうか。

 

 

最後までお読みいただきありがとうございました。

 

↓よければ投票ポチッとお願いします!

戦国IXA(イクサ)ランキング

 

 

スポンサーリンク