From 249394f548c652434f62b414c313904d236945fc Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 14 Jan 2026 23:04:52 +0900 Subject: [PATCH] =?UTF-8?q?test:=20GCD=20=EC=8B=9C=EB=AE=AC=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다양한 GCD 값(0~3000ms)에 대한 전투 효율성 비교 - 레벨별 GCD 영향 분석 테스트 - DPS, 스킬 사용 빈도, 전투 시간 측정 - 권장 GCD 분석 결과: 1500~2000ms --- test/core/engine/gcd_simulation_test.dart | 260 ++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 test/core/engine/gcd_simulation_test.dart diff --git a/test/core/engine/gcd_simulation_test.dart b/test/core/engine/gcd_simulation_test.dart new file mode 100644 index 0000000..0d72043 --- /dev/null +++ b/test/core/engine/gcd_simulation_test.dart @@ -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 = {}; + + 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; +}