Files
asciinevrdie/test/core/util/balance_analysis_test.dart
2026-01-14 23:07:03 +09:00

219 lines
7.6 KiB
Dart

// 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,
);
}