Files
asciinevrdie/test/core/engine/death_handler_test.dart
JiWoong Sul 68a5848510 style(lint): flutter analyze 경고 91건 → 0건 전체 정리
- analysis_options.yaml: freezed/g.dart 생성 파일 분석 제외
- game_text_l10n.dart: if문 중괄호 추가, 불필요한 ${} 제거
- iap_service.dart: 불필요한 dart:typed_data import 제거
- pq_logic.dart, combat_text_frames.dart: dangling library doc → library; 추가
- save_picker_dialog.dart: __ → _ (unnecessary_underscores)
- desktop_equipment_panel.dart: 불필요한 import 제거
- test 파일: _localVar → localVar 네이밍, ignore_for_file 추가
2026-03-31 14:22:09 +09:00

355 lines
12 KiB
Dart

import 'package:asciineverdie/src/core/engine/death_handler.dart';
import 'package:asciineverdie/src/core/model/equipment_item.dart';
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
import 'package:asciineverdie/src/core/model/game_state.dart';
import 'package:asciineverdie/src/core/model/item_stats.dart';
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/mock_factories.dart';
void main() {
const handler = DeathHandler();
/// 테스트용 장비 생성 헬퍼
EquipmentItem makeItem(String name, EquipmentSlot slot) {
return EquipmentItem(
name: name,
slot: slot,
level: 5,
weight: 3,
stats: const ItemStats(def: 10),
rarity: ItemRarity.rare,
);
}
/// 모든 슬롯에 장비가 장착된 Equipment 생성
Equipment fullyEquipped() {
return Equipment(
items: [
EquipmentItem.defaultWeapon(), // 0: 무기(weapon)
makeItem('Iron Shield', EquipmentSlot.shield),
makeItem('Iron Helm', EquipmentSlot.helm),
makeItem('Iron Hauberk', EquipmentSlot.hauberk),
makeItem('Iron Brassairts', EquipmentSlot.brassairts),
makeItem('Iron Vambraces', EquipmentSlot.vambraces),
makeItem('Iron Gauntlets', EquipmentSlot.gauntlets),
makeItem('Iron Gambeson', EquipmentSlot.gambeson),
makeItem('Iron Cuisses', EquipmentSlot.cuisses),
makeItem('Iron Greaves', EquipmentSlot.greaves),
makeItem('Iron Sollerets', EquipmentSlot.sollerets),
],
bestIndex: 0,
);
}
/// 테스트용 GameState 생성
///
/// [seed]: 결정론적 랜덤 시드(deterministic random seed)
/// [level]: 캐릭터 레벨
/// [isBossFight]: 보스전(boss fight) 여부
/// [equipment]: 커스텀 장비
GameState createState({
int seed = 42,
int level = 1,
bool isBossFight = false,
Equipment? equipment,
}) {
final state = GameState(
rng: DeterministicRandom(seed),
traits: Traits.empty().copyWith(level: level),
equipment: equipment ?? fullyEquipped(),
progress: ProgressState.empty().copyWith(
currentCombat: MockFactories.createCombat(),
finalBossState: isBossFight
? FinalBossState.fighting
: FinalBossState.notSpawned,
),
inventory: const Inventory(gold: 5000, items: []),
);
return state;
}
group('DeathHandler', () {
group('deathInfo 생성', () {
test('사망 시 deathInfo가 올바르게 생성된다', () {
// 준비(arrange): Lv10 캐릭터 - 100% 장비 손실 확률
final state = createState(level: 10);
// 실행(act)
final result = handler.processPlayerDeath(
state,
killerName: 'Dark Goblin',
cause: DeathCause.monster,
);
// 검증(assert): deathInfo 필드 확인
expect(result.deathInfo, isNotNull);
expect(result.deathInfo!.cause, DeathCause.monster);
expect(result.deathInfo!.killerName, 'Dark Goblin');
expect(result.deathInfo!.goldAtDeath, 5000);
expect(result.deathInfo!.levelAtDeath, 10);
expect(result.isDead, isTrue);
});
test('selfDamage 원인으로 사망 시 cause가 올바르다', () {
final state = createState(level: 1);
final result = handler.processPlayerDeath(
state,
killerName: 'Self',
cause: DeathCause.selfDamage,
);
expect(result.deathInfo!.cause, DeathCause.selfDamage);
});
});
group('보스전(boss fight) 사망', () {
test('보스전 사망 시 장비가 보호된다 (lostCount == 0)', () {
final equipment = fullyEquipped();
final state = createState(
level: 10,
isBossFight: true,
equipment: equipment,
);
final result = handler.processPlayerDeath(
state,
killerName: 'Final Boss',
cause: DeathCause.monster,
);
// 검증: 장비 손실 없음
expect(result.deathInfo!.lostEquipmentCount, 0);
expect(result.deathInfo!.lostItemName, isNull);
expect(result.deathInfo!.lostItem, isNull);
// 검증: 모든 장비가 그대로 유지
for (var i = 0; i < Equipment.slotCount; i++) {
expect(
result.equipment.getItemByIndex(i).name,
equipment.getItemByIndex(i).name,
);
}
});
test('보스전 사망 시 bossLevelingEndTime이 설정된다 (5분)', () {
final state = createState(isBossFight: true);
final before = DateTime.now().millisecondsSinceEpoch;
final result = handler.processPlayerDeath(
state,
killerName: 'Final Boss',
cause: DeathCause.monster,
);
final after = DateTime.now().millisecondsSinceEpoch;
final endTime = result.progress.bossLevelingEndTime;
// 검증: 5분(300,000ms) 후 시간이 설정됨
expect(endTime, isNotNull);
// before + 5분 <= endTime <= after + 5분 (타이밍 허용)
const fiveMinMs = 5 * 60 * 1000;
expect(endTime!, greaterThanOrEqualTo(before + fiveMinMs));
expect(endTime, lessThanOrEqualTo(after + fiveMinMs));
});
test('일반 사망 시 bossLevelingEndTime은 null이다', () {
final state = createState(level: 1);
final result = handler.processPlayerDeath(
state,
killerName: 'Goblin',
cause: DeathCause.monster,
);
expect(result.progress.bossLevelingEndTime, isNull);
});
});
group('장비 손실(equipment loss) 확률', () {
test('Lv1: 20% 확률 - roll < 20이면 장비 손실', () {
// Lv1 공식: 20 + (1-1)*80/9 = 20%
// 시드를 탐색하여 roll < 20인 경우를 찾아야 함
// DeterministicRandom(seed).nextInt(100)의 결과가 20 미만인 시드 사용
int? lossySeed;
for (var s = 0; s < 1000; s++) {
final rng = DeterministicRandom(s);
final roll = rng.nextInt(100);
if (roll < 20) {
lossySeed = s;
break;
}
}
expect(lossySeed, isNotNull, reason: '장비 손실 시드를 찾을 수 없음');
final state = createState(seed: lossySeed!, level: 1);
final result = handler.processPlayerDeath(
state,
killerName: 'Goblin',
cause: DeathCause.monster,
);
expect(result.deathInfo!.lostEquipmentCount, 1);
});
test('Lv1: 20% 확률 - roll >= 20이면 장비 보호', () {
// roll >= 20인 시드 찾기
int? safeSeed;
for (var s = 0; s < 1000; s++) {
final rng = DeterministicRandom(s);
final roll = rng.nextInt(100);
if (roll >= 20) {
safeSeed = s;
break;
}
}
expect(safeSeed, isNotNull, reason: '장비 보호 시드를 찾을 수 없음');
final state = createState(seed: safeSeed!, level: 1);
final result = handler.processPlayerDeath(
state,
killerName: 'Goblin',
cause: DeathCause.monster,
);
expect(result.deathInfo!.lostEquipmentCount, 0);
expect(result.deathInfo!.lostItemName, isNull);
});
test('Lv10+: 100% 확률로 장비 손실', () {
// Lv10 이상은 항상 100% 손실
final state = createState(seed: 42, level: 10);
final result = handler.processPlayerDeath(
state,
killerName: 'Dragon',
cause: DeathCause.monster,
);
expect(result.deathInfo!.lostEquipmentCount, 1);
expect(result.deathInfo!.lostItemName, isNotNull);
expect(result.deathInfo!.lostItemSlot, isNotNull);
expect(result.deathInfo!.lostItem, isNotNull);
});
test('Lv5: 중간 확률 (~55%) 공식 검증', () {
// Lv5 공식: 20 + (5-1)*80/9 = 20 + 35 = 55
// (정수 나눗셈: (4*80)~/9 = 320~/9 = 35)
final level = 5;
final expected = 20 + ((level - 1) * 80 ~/ 9);
expect(expected, 55);
});
});
group('무기(weapon) 슬롯 보호', () {
test('무기(슬롯 0)는 손실 대상에서 제외된다', () {
// Lv10(100% 손실)으로 여러 시드를 반복 테스트
// 무기는 절대 사라지지 않아야 함
for (var s = 0; s < 50; s++) {
final state = createState(seed: s, level: 10);
final result = handler.processPlayerDeath(
state,
killerName: 'Monster',
cause: DeathCause.monster,
);
// 무기(weapon)는 항상 유지
expect(
result.equipment.weaponItem.isNotEmpty,
isTrue,
reason: 'seed=$s에서 무기가 사라짐',
);
// 손실된 아이템이 있다면 weapon 슬롯이 아니어야 함
if (result.deathInfo!.lostItemSlot != null) {
expect(
result.deathInfo!.lostItemSlot,
isNot(EquipmentSlot.weapon),
reason: 'seed=$s에서 무기 슬롯이 손실됨',
);
}
}
});
});
group('빈 장비(empty equipment)', () {
test('무기 외 모든 장비가 비어있으면 손실 없음', () {
// 무기만 있고 나머지는 빈 장비
final emptyEquipment = Equipment.empty();
// Equipment.empty()는 기본 무기(Keyboard)만 있음
final state = createState(
seed: 42,
level: 10, // 100% 손실 확률
equipment: emptyEquipment,
);
final result = handler.processPlayerDeath(
state,
killerName: 'Monster',
cause: DeathCause.monster,
);
// 검증: 빈 슬롯만 있으므로 손실 불가
expect(result.deathInfo!.lostEquipmentCount, 0);
expect(result.deathInfo!.lostItemName, isNull);
});
});
group('deathCount 증가', () {
test('사망 시 deathCount가 1 증가한다', () {
final state = createState();
expect(state.progress.deathCount, 0);
final result = handler.processPlayerDeath(
state,
killerName: 'Goblin',
cause: DeathCause.monster,
);
expect(result.progress.deathCount, 1);
});
test('연속 사망 시 deathCount가 누적된다', () {
// 첫 번째 사망 (deathCount: 0 -> 1)
var state = createState(seed: 100);
state = handler.processPlayerDeath(
state,
killerName: 'Goblin',
cause: DeathCause.monster,
);
expect(state.progress.deathCount, 1);
// 부활 후 전투 재개 시뮬레이션 (deathInfo 초기화)
state = state.copyWith(clearDeathInfo: true);
// 두 번째 사망 (deathCount: 1 -> 2)
state = handler.processPlayerDeath(
state,
killerName: 'Dragon',
cause: DeathCause.monster,
);
expect(state.progress.deathCount, 2);
});
});
group('currentCombat 초기화', () {
test('사망 후 currentCombat이 null로 초기화되어야 한다', () {
// 전투 중(combat active) 상태에서 사망
final state = createState();
expect(state.progress.currentCombat, isNotNull);
final result = handler.processPlayerDeath(
state,
killerName: 'Monster',
cause: DeathCause.monster,
);
// clearCurrentCombat 파라미터로 전투 상태 초기화
expect(result.progress.currentCombat, isNull);
});
});
});
}