Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/PHASE5_6_CODE_INSPECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@

| # | 항목 | 판정 | 증거 |
| --- | --- | --- | --- |
| 1 | 트리거 6종 | **부분** | 5분 침묵(300s) ✓ (`firstFloorLongSilenceShown`, 5343행). 실패 escalator ✓ (PlayMode `FailedBaseCastsEscalateMagicNoteHints`). **1층 8초 침묵 트리거는 코드에서 발견 못 함** |
| 1 | 트리거 6종 | **부분** | 5분 침묵(300s) ✓ (`firstFloorLongSilenceShown`, 5343행). 실패 escalator ✓ (PlayMode `FailedBaseCastsEscalateMagicNoteHints`). 1층 8초 침묵 ✓ — **정정(2026-06-12): `firstFloorGhostShown` 8초 트리거가 구현돼 있음** (ExamGameController 5331행, ghost gesture + 노트). 초기 검수의 grep 누락이었음 |
| 2 | 의도적 미등장 | 빌드 검증 필요 | 코드 구조상 모순 없음, 전수 확인은 플레이 필요 |
| 3 | 대사 풀 트리거당 3~5변주 ≈ 25줄 | **부분** | `MentorPresentationController`에 층별 멘토 프로필 5종(벽화 연구원·균열 감시자 등) 구현. 변주 수 전수 카운트는 미실시 |
| 4 | 등장 연출 (페이드인+머리 위 룬+`npc_appear`, 4층 가장자리 윈도우) | **부분** | `npc_appear` SFX ✓, 멘토 프레젠테이션 ✓. 4층 가장자리 윈도우는 4층 디자인 자체가 변경돼 스펙 재정의 필요 |
| 5 | EditMode 테스트 (쿨다운·1회성·풀 무작위) | **미구현** | EditMode 테스트 5클래스 중 멘토 트리거 테스트 없음 (CustomShape/Gesture/PlayerMovement/StageDefinition/StrokeInput뿐) |
| 5 | EditMode 테스트 (쿨다운·1회성·풀 무작위) | **부분 (2026-06-12 갱신)** | `HintAssistanceTests` 추가 — escalator 단계 상승·캡·assisted 플래그·체크리스트·멘토 프로필을 EditMode로 검증. 단 "쿨다운·풀 무작위"는 **대사 풀/쿨다운 시스템 자체가 미구현**이라 테스트 대상이 없음 (스펙 §13 잔여 기능) |

## P6-2. 결정적 모먼트 검수

Expand Down
112 changes: 112 additions & 0 deletions unity/MagicExamHall/Assets/Tests/EditMode/HintAssistanceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using MagicExamHall;
using NUnit.Framework;

namespace MagicExamHall.Tests
{
public sealed class HintAssistanceTests
{
[Test]
public void FirstFailureStartsAtReasonHintAndEscalatesToGhostTraceCap()
{
Assert.That(HintAssistance.ResolveLevel(priorFailures: 0, success: false), Is.EqualTo(AssistLevel.ReasonHint));
Assert.That(HintAssistance.ResolveLevel(priorFailures: 1, success: false), Is.EqualTo(AssistLevel.Checklist));
Assert.That(HintAssistance.ResolveLevel(priorFailures: 2, success: false), Is.EqualTo(AssistLevel.GhostTrace));
Assert.That(HintAssistance.ResolveLevel(priorFailures: 7, success: false), Is.EqualTo(AssistLevel.GhostTrace), "escalator must cap at GhostTrace");
}

[Test]
public void CleanSuccessNeverShowsAssistance()
{
Assert.That(HintAssistance.ResolveLevel(priorFailures: 0, success: true), Is.EqualTo(AssistLevel.None));

var state = HintAssistance.ForAttempt(SpellFamily.Fire, priorFailures: 0, success: true, result: null);
Assert.That(state.hintShown, Is.False);
Assert.That(state.assisted, Is.False);
Assert.That(state.currentLevel, Is.EqualTo(AssistLevel.None));
}

[Test]
public void SuccessAfterFailuresKeepsAssistedFlagAndReachedLevel()
{
var state = HintAssistance.ForAttempt(SpellFamily.Water, priorFailures: 2, success: true, result: null);

Assert.That(state.assisted, Is.True, "a success that needed prior failures counts as assisted");
Assert.That(state.currentLevel, Is.EqualTo(AssistLevel.Checklist));
Assert.That(state.hintShown, Is.True);
}

[Test]
public void HintShownAlwaysMatchesLevel()
{
foreach (SpellFamily family in Enum.GetValues(typeof(SpellFamily)))
{
for (var failures = 0; failures <= 4; failures++)
{
var state = HintAssistance.PreviewFor(family, failures);
Assert.That(state.hintShown, Is.EqualTo(state.currentLevel != AssistLevel.None),
$"hintShown must mirror level for {family} with {failures} failures");
Assert.That(state.body, Is.Not.Empty, $"every hint state needs body text ({family}, {failures})");
}
}
}

[Test]
public void EveryFamilyHasAThreeItemChecklistWithActionVerbs()
{
foreach (SpellFamily family in Enum.GetValues(typeof(SpellFamily)))
{
var checklist = HintAssistance.ChecklistFor(family);
Assert.That(checklist.Count, Is.EqualTo(3), $"{family} checklist must have 3 steps");
foreach (var item in checklist)
{
Assert.That(item.Trim(), Is.Not.Empty);
Assert.That(item.Trim().Length, Is.LessThanOrEqualTo(30), $"checklist lines stay short: '{item}'");
}
}
}

[Test]
public void ChecklistLevelBodyJoinsTheFamilyChecklist()
{
var state = HintAssistance.PreviewFor(SpellFamily.Earth, priorFailures: 2);

Assert.That(state.currentLevel, Is.EqualTo(AssistLevel.Checklist));
foreach (var item in HintAssistance.ChecklistFor(SpellFamily.Earth))
{
Assert.That(state.body, Does.Contain(item));
}
}

[Test]
public void MentorProfilesAreDistinctPerFloorAndFallBackToFloorOne()
{
var names = new System.Collections.Generic.HashSet<string>();
for (var floor = 1; floor <= 5; floor++)
{
var profile = MentorProfile.ForFloor(floor);
Assert.That(profile.name, Is.Not.Empty);
Assert.That(names.Add(profile.name), Is.True, $"floor {floor} mentor name must be unique");
}

Assert.That(MentorProfile.ForFloor(99).name, Is.EqualTo(MentorProfile.ForFloor(1).name));
Assert.That(MentorProfile.ForFloor(-1).name, Is.EqualTo(MentorProfile.ForFloor(1).name));
}

[Test]
public void MentorProfileMapsEveryMoodToASprite()
{
for (var floor = 1; floor <= 5; floor++)
{
var profile = MentorProfile.ForFloor(floor);
foreach (MentorMood mood in Enum.GetValues(typeof(MentorMood)))
{
Assert.That((int)profile.KindFor(mood), Is.GreaterThanOrEqualTo(0));
}

Assert.That(profile.KindFor(MentorMood.Happy), Is.Not.EqualTo(profile.KindFor(MentorMood.Frown)),
$"floor {floor} mentor needs distinct happy/frown sprites");
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.