/**
* — シミュレーションの設定 —
* ここにある数字を書き換えるだけで、色々なパターンを試せるよ!
*/
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 });
}
条件を変えることで、敵が3部隊の砦の攻略や「気丈な決意」登場後の砦の攻略、こちらの攻撃部隊を1つ増やした場合や伊達藤次郎のTRを上げて待機時間を短くした場合、ビビを部隊に加えた場合、なども計算できます。
大がかりな変更があれば、AIに丸投げする方が早いですね。
ディスカッション
コメント一覧
まだ、コメントがありません