/// 원본 대비 회귀 테스트 (Regression Test) /// /// 동일 시드에서 원본 Progress Quest와 동일한 결과가 나오는지 확인합니다. /// 이 테스트는 게임 로직의 결정적(deterministic) 특성을 보장합니다. library; import 'package:askiineverdie/src/core/engine/game_mutations.dart'; import 'package:askiineverdie/src/core/engine/progress_service.dart'; import 'package:askiineverdie/src/core/engine/reward_service.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/core/model/pq_config.dart'; import 'package:askiineverdie/src/core/util/deterministic_random.dart'; import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic; import 'package:flutter_test/flutter_test.dart'; void main() { const config = PqConfig(); final mutations = GameMutations(config); late ProgressService service; setUp(() { service = ProgressService( config: config, mutations: mutations, rewards: RewardService(mutations), ); }); group('Deterministic Game Flow - Seed 42', () { // 고정 시드 42에서의 예상 결과값 (스냅샷) const testSeed = 42; test('generateName produces consistent names', () { // 시드 42에서 생성되는 이름들 검증 expect(pq_logic.generateName(DeterministicRandom(testSeed)), 'Bregpran'); expect(pq_logic.generateName(DeterministicRandom(100)), 'Grotlaex'); expect(pq_logic.generateName(DeterministicRandom(999)), 'Idfrok'); }); test('monsterTask produces consistent monster names', () { // 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트 // 시드 42, 레벨 5에서의 몬스터 이름 final monster1 = pq_logic .monsterTask(config, DeterministicRandom(testSeed), 5, null, null) .displayName; expect(monster1, isNotEmpty); // 시드 42, 레벨 10에서의 몬스터 이름 final monster2 = pq_logic .monsterTask(config, DeterministicRandom(testSeed), 10, null, null) .displayName; expect(monster2, isNotEmpty); // 시드 42, 레벨 1에서의 몬스터 이름 final monster3 = pq_logic .monsterTask(config, DeterministicRandom(testSeed), 1, null, null) .displayName; expect(monster3, isNotEmpty); }); test('winEquip produces consistent equipment', () { // 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트 // 시드 42에서 무기 획득 (슬롯 0) final weapon = pq_logic.winEquip( config, DeterministicRandom(testSeed), 5, 0, // weapon slot ); expect(weapon, isNotEmpty); // 시드 42에서 방어구 획득 (슬롯 2 = helm, armor 카테고리) final armor = pq_logic.winEquip( config, DeterministicRandom(testSeed), 5, 2, // helm slot (armor category) ); expect(armor, isNotEmpty); // 시드 42에서 방패 획득 (슬롯 1) final shield = pq_logic.winEquip( config, DeterministicRandom(testSeed), 5, 1, // shield slot ); expect(shield, isNotEmpty); }); test('winSpell produces consistent spells', () { // 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트 // 시드 42에서 주문 획득 (레벨 5, 지능 10) final spell1 = pq_logic.winSpell( config, DeterministicRandom(testSeed), 5, 10, ); expect(spell1, isNotEmpty); expect(spell1, contains('|')); // 시드 100에서 주문 획득 final spell2 = pq_logic.winSpell( config, DeterministicRandom(100), 10, 15, ); expect(spell2, isNotEmpty); }); test('winItem produces consistent items', () { // 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트 // 시드 42에서 아이템 획득 final item1 = pq_logic.winItem( config, DeterministicRandom(testSeed), 5, ); expect(item1, isNotEmpty); expect(item1, contains(' of ')); // 시드 100에서 아이템 획득 final item2 = pq_logic.winItem(config, DeterministicRandom(100), 10); expect(item2, isNotEmpty); }); test('completeQuest produces consistent rewards', () { // 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트 // 시드 42에서 퀘스트 완료 final quest = pq_logic.completeQuest( config, DeterministicRandom(testSeed), 5, ); expect(quest.caption, isNotEmpty); expect(quest.reward, pq_logic.RewardKind.spell); // 시드 100에서 퀘스트 완료 final quest2 = pq_logic.completeQuest( config, DeterministicRandom(100), 3, ); expect(quest2.caption, isNotEmpty); expect(quest2.reward, pq_logic.RewardKind.stat); }); test('interplotCinematic produces consistent storylines', () { // 시드 42에서 시네마틱 이벤트 final entries = pq_logic.interplotCinematic( config, DeterministicRandom(testSeed), 10, 3, ); // 첫 번째 엔트리 확인 (시나리오 타입에 따라 다름) expect(entries.isNotEmpty, isTrue); // 아스키나라(ASCII-Nara) 세계관: 'Compiling' expect(entries.last.caption, 'Compiling'); expect(entries.last.kind, QueueKind.plot); }); test('namedMonster produces consistent named monsters', () { // 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트 final monster1 = pq_logic.namedMonster( config, DeterministicRandom(testSeed), 10, ); expect(monster1, contains(' the ')); final monster2 = pq_logic.namedMonster( config, DeterministicRandom(100), 5, ); expect(monster2, contains(' the ')); }); test('impressiveGuy produces consistent NPC titles', () { final guy1 = pq_logic.impressiveGuy( config, DeterministicRandom(testSeed), ); // 시드 42에서의 NPC 타이틀 확인 expect(guy1.isNotEmpty, isTrue); expect(guy1, contains('of')); final guy2 = pq_logic.impressiveGuy(config, DeterministicRandom(100)); expect(guy2.isNotEmpty, isTrue); }); }); group('Game State Progression - Deterministic', () { test('initial game state is consistent for same seed', () { final state1 = GameState.withSeed( seed: 42, traits: const Traits( name: 'Hero', race: 'Elf', klass: 'Mage', level: 1, motto: '', guild: '', ), stats: const Stats( str: 10, con: 10, dex: 10, intelligence: 10, wis: 10, cha: 10, hpMax: 20, mpMax: 15, ), ); final state2 = GameState.withSeed( seed: 42, traits: const Traits( name: 'Hero', race: 'Elf', klass: 'Mage', level: 1, motto: '', guild: '', ), stats: const Stats( str: 10, con: 10, dex: 10, intelligence: 10, wis: 10, cha: 10, hpMax: 20, mpMax: 15, ), ); // 동일 시드로 생성된 상태는 동일한 RNG 시퀀스를 가짐 expect(state1.rng.nextInt(100), state2.rng.nextInt(100)); }); test('tick produces consistent state changes', () { final initialState = GameState.withSeed( seed: 42, traits: const Traits( name: 'TestHero', race: 'Human', klass: 'Fighter', level: 1, motto: '', guild: '', ), stats: const Stats( str: 12, con: 10, dex: 8, intelligence: 6, wis: 7, cha: 9, hpMax: 10, mpMax: 8, ), progress: const ProgressState( task: ProgressBarState(position: 0, max: 1000), quest: ProgressBarState(position: 0, max: 10000), plot: ProgressBarState(position: 0, max: 36000), exp: ProgressBarState(position: 0, max: 1269), encumbrance: ProgressBarState(position: 0, max: 22), currentTask: TaskInfo(caption: 'Battle', type: TaskType.kill), plotStageCount: 1, questCount: 0, ), ); // 첫 번째 틱 (100ms) final result1 = service.tick(initialState, 100); // 동일한 초기 상태에서 동일한 증분으로 틱 final result1b = service.tick(initialState, 100); // 태스크 진행이 동일해야 함 expect( result1.state.progress.task.position, result1b.state.progress.task.position, ); }); test('levelUp stat gains are deterministic', () { // 레벨업 시 스탯 증가 검증 final baseStats = const Stats( str: 10, con: 10, dex: 10, intelligence: 10, wis: 10, cha: 10, hpMax: 20, mpMax: 15, ); // 동일 시드에서 winStat은 동일한 결과를 반환해야 함 final stat1 = pq_logic.winStat(baseStats, DeterministicRandom(42)); final stat2 = pq_logic.winStat(baseStats, DeterministicRandom(42)); expect(stat1.str, stat2.str); expect(stat1.con, stat2.con); expect(stat1.dex, stat2.dex); expect(stat1.intelligence, stat2.intelligence); expect(stat1.wis, stat2.wis); expect(stat1.cha, stat2.cha); }); }); group('Config Data Integrity', () { test('races list matches original count', () { // 원본 Config.dfm의 Races 개수: 21 expect(config.races.length, 21); }); test('klasses list matches original count', () { // 원본 Config.dfm의 Klasses 개수: 18 expect(config.klasses.length, 18); }); test('monsters list matches ASCII-Nara count', () { // 아스키나라(ASCII-Nara) 세계관 몬스터 개수: 304 expect(config.monsters.length, 304); }); test('spells list is not empty', () { expect(config.spells.isNotEmpty, isTrue); }); test('weapons list is not empty', () { expect(config.weapons.isNotEmpty, isTrue); }); test('armors list is not empty', () { expect(config.armors.isNotEmpty, isTrue); }); test('shields list is not empty', () { expect(config.shields.isNotEmpty, isTrue); }); }); }