// ignore_for_file: avoid_print import 'package:asciineverdie/src/core/model/item_stats.dart'; import 'package:asciineverdie/src/core/util/balance_constants.dart'; import 'package:flutter_test/flutter_test.dart'; /// 전체 레벨 밸런스 분석 테스트 /// /// 이 테스트는 플레이어와 몬스터의 스탯 스케일링을 분석하고 /// 예상 생존율을 계산합니다. void main() { group('전체 레벨 밸런스 분석', () { test('레벨별 몬스터 vs 플레이어 스탯 비교', () { print('\n${'=' * 80}'); print('레벨별 밸런스 분석 리포트'); print('${'=' * 80}\n'); // 분석할 레벨 final levels = [1, 5, 8, 10, 20, 30, 50, 80, 100]; print('### 1. 몬스터 스탯 (현재 공식)'); print('| 레벨 | HP | ATK | DEF | EXP | 타입 |'); print('|------|-----|-----|-----|-----|------|'); for (final level in levels) { final m = MonsterBaseStats.forLevel(level); print( '| ${level.toString().padLeft(4)} | ${m.hp.toString().padLeft(4)} ' '| ${m.atk.toString().padLeft(3)} | ${m.def.toString().padLeft(3)} ' '| ${m.exp.toString().padLeft(3)} | Normal |', ); } print('\n### 2. 플레이어 예상 스탯 (장비 포함)'); print('| 레벨 | HP | ATK | DEF | 장비DEF |'); print('|------|-----|-----|-----|---------|'); for (final level in levels) { final player = _estimatePlayerStats(level); print( '| ${level.toString().padLeft(4)} | ${player.hp.toString().padLeft(4)} ' '| ${player.atk.toString().padLeft(3)} | ${player.def.toString().padLeft(3)} ' '| ${player.equipDef.toString().padLeft(7)} |', ); } print('\n### 3. 전투 시뮬레이션 (평균 데미지)'); print('| 레벨 | 몬→플 | 플→몬 | 플생존 | 몬생존 | 승률예상 |'); print('|------|-------|-------|--------|--------|----------|'); for (final level in levels) { final sim = _simulateCombat(level); print( '| ${level.toString().padLeft(4)} ' '| ${sim.monsterDamage.toString().padLeft(5)} ' '| ${sim.playerDamage.toString().padLeft(5)} ' '| ${sim.playerHits.toString().padLeft(6)} ' '| ${sim.monsterHits.toString().padLeft(6)} ' '| ${(sim.winRate * 100).toStringAsFixed(0).padLeft(8)}% |', ); } print('\n### 4. 밸런스 진단'); for (final level in levels) { final sim = _simulateCombat(level); final tier = LevelTierSettings.forLevel(level); final targetWinRate = 1.0 - tier.targetDeathRate; final diagnosis = sim.winRate >= targetWinRate ? '✓ 적정' : '✗ 조정필요 (목표: ${(targetWinRate * 100).toStringAsFixed(0)}%)'; print('레벨 $level (${tier.name}): 승률 ${(sim.winRate * 100).toStringAsFixed(0)}% $diagnosis'); } print('\n${'=' * 80}'); }); test('문제점 및 개선 제안', () { print('\n### 현재 밸런스 문제점\n'); // 레벨 8 상세 분석 (사용자 보고 케이스) final level = 8; final monster = MonsterBaseStats.forLevel(level); final player = _estimatePlayerStats(level); print('#### 레벨 8 상세 분석 (사용자 보고 케이스)'); print('- 몬스터 ATK: ${monster.atk}'); print('- 플레이어 HP: ${player.hp}'); print('- 플레이어 DEF: ${player.def}'); // 데미지 계산 (combat_calculator 공식) // damage = ATK * 0.8~1.2 - DEF * 0.3 final minDamage = (monster.atk * 0.8 - player.def * 0.3).round(); final maxDamage = (monster.atk * 1.2 - player.def * 0.3).round(); print('- 몬스터 데미지: $minDamage ~ $maxDamage'); print('- 플레이어 생존 히트: ${(player.hp / maxDamage).floor()} ~ ${(player.hp / minDamage).ceil()}'); print('\n#### 적용된 밸런스 수정'); print('1. 플레이어 HP 스케일링 상향:'); print(' - hpPerLevel: 12 → 18 (50% 증가)'); print(' - hpPerCon: 6 → 10 (67% 증가)'); print('2. 데미지 공식 DEF 감산율 상향:'); print(' - defenderDef * 0.3 → defenderDef * 0.5 (방어력 효과 67% 증가)'); print('3. 몬스터 ATK 대폭 하향:'); print(' - 5 + level * 6 → 3 + level * 4 (33% 감소)'); }); }); } /// 플레이어 예상 스탯 class _PlayerEstimate { final int hp; final int atk; final int def; final int equipDef; _PlayerEstimate({ required this.hp, required this.atk, required this.def, required this.equipDef, }); } /// 레벨별 플레이어 스탯 추정 /// /// 가정: /// - 평균 스탯: STR/CON/DEX = 15 (3d6 평균 10.5 + 종족/클래스 보정) /// - 장비: 레벨 * 0.8 수준의 평균 장비 _PlayerEstimate _estimatePlayerStats(int level) { // 기본 스탯 (평균) const str = 15; const con = 15; // dex는 회피/크리티컬에 영향, 여기서는 HP/ATK/DEF만 분석 // 기본 HP (종족별 다르지만 평균 ~10 가정) const baseHp = 10; // PlayerScaling 공식: hpMax = baseHp + (level - 1) * hpPerLevel + conBonus * hpPerCon // 수정: hpPerLevel 12→18, hpPerCon 6→10 final hpMax = baseHp + (level - 1) * 18 + (con - 10) * 10; // 장비 스탯 추정 (레벨 * 0.8 수준의 common 장비 10개) final equipLevel = (level * 0.8).round().clamp(1, level); final equipBaseValue = (equipLevel * 1.2 * ItemRarity.common.multiplier).round(); // 무기 ATK (speedMultiplier 1.0 가정) final weaponAtk = equipBaseValue; // 방어구 DEF (10개 슬롯, 평균 multiplier 0.85) final armorDef = (equipBaseValue * 0.85 * 10).round(); // CombatStats.fromStats 공식 // baseAtk = effectiveStr * 2 + level + equipStats.atk final baseAtk = str * 2 + level + weaponAtk; // baseDef = effectiveCon + (level ~/ 2) + equipStats.def final baseDef = con + (level ~/ 2) + armorDef; return _PlayerEstimate( hp: hpMax, atk: baseAtk, def: baseDef, equipDef: armorDef, ); } /// 전투 시뮬레이션 결과 class _CombatSimulation { final int monsterDamage; final int playerDamage; final int playerHits; // 플레이어가 버틸 수 있는 히트 수 final int monsterHits; // 몬스터를 죽이는데 필요한 히트 수 final double winRate; _CombatSimulation({ required this.monsterDamage, required this.playerDamage, required this.playerHits, required this.monsterHits, required this.winRate, }); } /// 전투 시뮬레이션 _CombatSimulation _simulateCombat(int level) { final monster = MonsterBaseStats.forLevel(level); final player = _estimatePlayerStats(level); // 데미지 계산 (combat_calculator 평균) // damage = ATK * 1.0 - DEF * 0.5 (DEF 감산율 상향) final monsterDamage = (monster.atk * 1.0 - player.def * 0.5).round().clamp(1, 9999); final playerDamage = (player.atk * 1.0 - monster.def * 0.5).round().clamp(1, 9999); // 생존 히트 수 final playerHits = (player.hp / monsterDamage).ceil(); final monsterHits = (monster.hp / playerDamage).ceil(); // 승률 추정 (히트 비율 기반) // 플레이어가 먼저 공격한다고 가정하면, playerHits > monsterHits면 승리 final winRate = playerHits > monsterHits ? 0.95 // 압도적 유리 : playerHits == monsterHits ? 0.65 // 동등 (선공 이점) : (playerHits / monsterHits).clamp(0.2, 0.9); return _CombatSimulation( monsterDamage: monsterDamage, playerDamage: playerDamage, playerHits: playerHits, monsterHits: monsterHits, winRate: winRate, ); }