test: GCD 시뮬레이션 테스트 추가

- 다양한 GCD 값(0~3000ms)에 대한 전투 효율성 비교
- 레벨별 GCD 영향 분석 테스트
- DPS, 스킬 사용 빈도, 전투 시간 측정
- 권장 GCD 분석 결과: 1500~2000ms
This commit is contained in:
JiWoong Sul
2026-01-14 23:04:52 +09:00
parent 85413362a2
commit 249394f548

View File

@@ -0,0 +1,260 @@
import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
/// 글로벌 쿨타임(GCD) 시뮬레이션 테스트
///
/// 다양한 GCD 값에 대해 전투 효율성을 측정
void main() {
test('GCD 시뮬레이션 - 다양한 값 비교', () {
print('\n' + '=' * 80);
print('글로벌 쿨타임(GCD) 시뮬레이션 결과');
print('=' * 80);
// 테스트할 GCD 값들 (ms)
final gcdValues = [0, 500, 1000, 1500, 2000, 2500, 3000];
// 시뮬레이션 파라미터
const playerAttackDelay = 1000; // 플레이어 기본 공격 딜레이
const monsterAttackDelay = 1200; // 몬스터 공격 딜레이
const skillUseProbability = 0.30; // 스킬 사용 확률 (30%)
const normalAttackDamage = 50; // 일반 공격 데미지
const skillDamage = 120; // 스킬 데미지 (평균)
const monsterHp = 1000; // 몬스터 HP
const simulationRuns = 1000; // 시뮬레이션 반복 횟수
final results = <int, SimulationResult>{};
for (final gcd in gcdValues) {
final result = _runSimulation(
gcd: gcd,
playerAttackDelay: playerAttackDelay,
monsterAttackDelay: monsterAttackDelay,
skillUseProbability: skillUseProbability,
normalAttackDamage: normalAttackDamage,
skillDamage: skillDamage,
monsterHp: monsterHp,
runs: simulationRuns,
);
results[gcd] = result;
}
// 결과 출력
print('\n### 시뮬레이션 조건');
print('- 플레이어 공격 딜레이: ${playerAttackDelay}ms');
print('- 몬스터 HP: $monsterHp');
print('- 일반 공격 데미지: $normalAttackDamage');
print('- 스킬 데미지 (평균): $skillDamage');
print('- 스킬 사용 확률: ${(skillUseProbability * 100).toInt()}%');
print('- 시뮬레이션 횟수: $simulationRuns회');
print('\n### 결과 비교');
print('| GCD (ms) | 전투시간 (ms) | 총 공격 | 스킬 사용 | 스킬비율 | DPS | 효율 |');
print(
'|----------|---------------|---------|-----------|----------|-----|------|',
);
final baselineDps = results[0]!.avgDps;
for (final gcd in gcdValues) {
final r = results[gcd]!;
final efficiency = (r.avgDps / baselineDps * 100).toStringAsFixed(0);
print(
'| ${gcd.toString().padLeft(8)} '
'| ${r.avgCombatTime.toStringAsFixed(0).padLeft(13)} '
'| ${r.avgTotalAttacks.toStringAsFixed(1).padLeft(7)} '
'| ${r.avgSkillUses.toStringAsFixed(1).padLeft(9)} '
'| ${(r.skillRatio * 100).toStringAsFixed(1).padLeft(7)}% '
'| ${r.avgDps.toStringAsFixed(1).padLeft(3)} '
'| ${efficiency.padLeft(4)}% |',
);
}
// 권장 GCD 분석
print('\n### 분석 및 권장');
// GCD 1500ms 기준 분석
final gcd1500 = results[1500]!;
final gcd2000 = results[2000]!;
// gcd1000은 비교용으로 추후 활용 가능
print('\n#### GCD별 특징');
print('- **0ms (없음)**: 기준선, 스킬 남용 가능');
print('- **500ms**: 거의 제한 없음, 빠른 연속 스킬 가능');
print('- **1000ms**: 공격 딜레이와 동일, 매 공격마다 스킬 선택 가능');
print('- **1500ms**: 스킬 후 1회 일반공격 강제, 적절한 제한');
print('- **2000ms**: 스킬 후 2회 일반공격 강제, 스킬이 특별해짐');
print('- **2500ms+**: 스킬 사용이 매우 제한적, 전략적 선택 필요');
print('\n#### 권장 GCD');
print('**1500ms ~ 2000ms 권장**');
print('- 스킬 사용 빈도가 자연스럽게 제한됨');
print('- 일반 공격의 중요성이 유지됨');
print(
'- DPS 손실이 크지 않음 (${((1 - gcd1500.avgDps / baselineDps) * 100).toStringAsFixed(0)}% ~ ${((1 - gcd2000.avgDps / baselineDps) * 100).toStringAsFixed(0)}%)',
);
print('\n' + '=' * 80);
// 테스트는 항상 통과 (정보 출력용)
expect(true, isTrue);
});
test('GCD 상세 시뮬레이션 - 레벨별 영향', () {
print('\n' + '=' * 80);
print('레벨별 GCD 영향 분석');
print('=' * 80);
// 레벨별 파라미터
final levelConfigs = [
{'level': 1, 'playerAtk': 30, 'skillDmg': 80, 'monsterHp': 200},
{'level': 10, 'playerAtk': 50, 'skillDmg': 130, 'monsterHp': 500},
{'level': 30, 'playerAtk': 90, 'skillDmg': 220, 'monsterHp': 1500},
{'level': 50, 'playerAtk': 130, 'skillDmg': 320, 'monsterHp': 3000},
];
const gcdToTest = [0, 1500, 2000];
print('\n| 레벨 | GCD | 전투시간 | 스킬횟수 | DPS | 효율 |');
print('|------|-----|----------|----------|-----|------|');
for (final config in levelConfigs) {
final level = config['level'] as int;
final playerAtk = config['playerAtk'] as int;
final skillDmg = config['skillDmg'] as int;
final monsterHp = config['monsterHp'] as int;
double? baselineDps;
for (final gcd in gcdToTest) {
final result = _runSimulation(
gcd: gcd,
playerAttackDelay: 1000,
monsterAttackDelay: 1200,
skillUseProbability: 0.30,
normalAttackDamage: playerAtk,
skillDamage: skillDmg,
monsterHp: monsterHp,
runs: 500,
);
baselineDps ??= result.avgDps;
final efficiency = (result.avgDps / baselineDps * 100).toStringAsFixed(
0,
);
print(
'| ${level.toString().padLeft(4)} '
'| ${gcd.toString().padLeft(3)} '
'| ${result.avgCombatTime.toStringAsFixed(0).padLeft(8)} '
'| ${result.avgSkillUses.toStringAsFixed(1).padLeft(8)} '
'| ${result.avgDps.toStringAsFixed(1).padLeft(3)} '
'| ${efficiency.padLeft(4)}% |',
);
}
}
print('\n' + '=' * 80);
expect(true, isTrue);
});
}
/// 시뮬레이션 실행
SimulationResult _runSimulation({
required int gcd,
required int playerAttackDelay,
required int monsterAttackDelay,
required double skillUseProbability,
required int normalAttackDamage,
required int skillDamage,
required int monsterHp,
required int runs,
}) {
final random = Random(42); // 재현 가능한 시드
var totalCombatTime = 0;
var totalAttacks = 0;
var totalSkillUses = 0;
var totalDamage = 0;
for (var i = 0; i < runs; i++) {
var currentHp = monsterHp;
var elapsedTime = 0;
var attackCount = 0;
var skillCount = 0;
var gcdRemaining = 0; // 남은 GCD 시간
var playerAccum = 0;
while (currentHp > 0) {
// 시간 진행 (100ms 단위)
const tickMs = 100;
elapsedTime += tickMs;
playerAccum += tickMs;
// GCD 감소
if (gcdRemaining > 0) {
gcdRemaining -= tickMs;
if (gcdRemaining < 0) gcdRemaining = 0;
}
// 플레이어 공격 체크
if (playerAccum >= playerAttackDelay) {
playerAccum -= playerAttackDelay;
attackCount++;
// 스킬 사용 여부 결정
final canUseSkill = gcdRemaining <= 0;
final wantsToUseSkill = random.nextDouble() < skillUseProbability;
if (canUseSkill && wantsToUseSkill) {
// 스킬 사용
currentHp -= skillDamage;
totalDamage += skillDamage;
skillCount++;
gcdRemaining = gcd; // GCD 시작
} else {
// 일반 공격
currentHp -= normalAttackDamage;
totalDamage += normalAttackDamage;
}
}
// 무한 루프 방지
if (elapsedTime > 60000) break;
}
totalCombatTime += elapsedTime;
totalAttacks += attackCount;
totalSkillUses += skillCount;
}
final avgCombatTime = totalCombatTime / runs;
final avgTotalAttacks = totalAttacks / runs;
final avgSkillUses = totalSkillUses / runs;
final skillRatio = avgSkillUses / avgTotalAttacks;
final avgDps = totalDamage / runs / (avgCombatTime / 1000);
return SimulationResult(
avgCombatTime: avgCombatTime,
avgTotalAttacks: avgTotalAttacks,
avgSkillUses: avgSkillUses,
skillRatio: skillRatio,
avgDps: avgDps,
);
}
class SimulationResult {
const SimulationResult({
required this.avgCombatTime,
required this.avgTotalAttacks,
required this.avgSkillUses,
required this.skillRatio,
required this.avgDps,
});
final double avgCombatTime;
final double avgTotalAttacks;
final double avgSkillUses;
final double skillRatio;
final double avgDps;
}