Adv 2.0 Phase L2 step 8: arena test cleanup + bossflow win assertion

Two cleanup edits to adventure_arena_test.go:
- TestRenderArenaStreakEntry_ShowsMultipliers: Level 50 → 20 (D&D cap).
- TestRenderArenaStreakEntry_HighLevel_AllEligible: stale "Level 70"
  comment retargeted to Level 20.
- TestArenaStreakSimulation_FullRun: rename combatLevel → skillBonus
  (the value flows into arenaRoundReward as the battleSkill arg, not
  a character level — naming was a CombatLevel-era leftover).

New test in adventure_arena_bossflow_test.go:
TestArenaBossOutcome_WinSurfacesStagedLogAndBossDeath drives
renderBossOutcome directly with a forced-win CombatResult so the
assertion is RNG-free, then checks the win path emits a TwinBee
BossDeath line before the victory headline plus the trailing d20 roll
summary. RenderCombatLog is exercised on the same result to confirm
the staged-log phases caller in resolveArenaBoss returns content.

go vet ./... + go test ./... clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-09 08:36:00 -07:00
parent aea307e418
commit eeb96c4b07
2 changed files with 67 additions and 6 deletions

View File

@@ -135,6 +135,67 @@ func TestBossFlowPhaseMessages(t *testing.T) {
}
}
// Phase L2 step 8 — assert that an arena win surfaces both the staged
// combat log (RenderCombatLog phases prepended by the arena intro) and a
// TwinBee BossDeath flavor line in the outcome block. Drives
// renderBossOutcome directly with a forced-win CombatResult so the
// assertion is RNG-free; the resolveArenaBoss smoke test above already
// exercises the live combat path end-to-end.
func TestArenaBossOutcome_WinSurfacesStagedLogAndBossDeath(t *testing.T) {
monster := arenaBosses[arenaBossID(1, 1)]
result := CombatResult{
PlayerWon: true,
PlayerStartHP: 60, PlayerEndHP: 42,
EnemyStartHP: monster.HP, EnemyEndHP: 0,
Events: []CombatEvent{
{Round: 1, Phase: "attack", Actor: "player", Action: "hit", Roll: 14, RollAgainst: monster.AC, Damage: 10, EnemyHP: monster.HP - 10, PlayerHP: 60},
{Round: 2, Phase: "attack", Actor: "player", Action: "crit", Roll: 20, RollAgainst: monster.AC, Damage: monster.HP - 10, EnemyHP: 0, PlayerHP: 42},
},
TotalRounds: 2,
}
// Staged combat log — same call resolveArenaBoss makes for `phases`.
phases := RenderCombatLog(result, "Champion", monster.Name)
if len(phases) == 0 {
t.Fatal("RenderCombatLog returned no phases for a winning result")
}
victoryHeadline := "🏆 **" + monster.Name + "** falls (HP " + "60" + "→" + "42" + " / 60)."
outcome := renderBossOutcome(BossOutcomeInputs{
ZoneID: ZoneArena,
RunID: "arena-stagedlog-test",
RoomIdx: 11,
Monster: monster,
Result: result,
PreHP: 60, PostHP: 42, MaxHP: 60,
PhaseTwoAt: arenaBossPhaseTwoAt(1),
Nat20s: 1,
Nat1s: 0,
DefeatHeadline: "unused",
VictoryHeadline: victoryHeadline,
})
if !strings.Contains(outcome, "🎭 **TwinBee:**") {
t.Errorf("outcome missing TwinBee narration line: %q", outcome)
}
if !strings.Contains(outcome, victoryHeadline) {
t.Errorf("outcome missing victory headline: %q", outcome)
}
if !strings.Contains(outcome, "🎲 d20 —") {
t.Errorf("outcome missing dice-roll summary: %q", outcome)
}
// BossDeath line must be present — the TwinBee line preceding the
// victory headline is sourced from flavor.BossDeath via twinBeeLine.
headlineIdx := strings.Index(outcome, victoryHeadline)
if headlineIdx <= 0 {
t.Fatalf("victory headline not after a TwinBee line: %q", outcome)
}
prefix := outcome[:headlineIdx]
if !strings.Contains(prefix, "🎭 **TwinBee:**") {
t.Errorf("BossDeath TwinBee line should precede victory headline; prefix=%q", prefix)
}
}
func TestArenaBosses_AllTiersPopulated(t *testing.T) {
for tier := 1; tier <= 5; tier++ {
for round := 1; round <= 4; round++ {

View File

@@ -458,7 +458,7 @@ func TestArenaGearDropRates_Decreasing(t *testing.T) {
// ── Render Tests (New/Updated) ─────────────────────────────────────────────
func TestRenderArenaStreakEntry_ShowsMultipliers(t *testing.T) {
char := &DnDCharacter{Level: 50}
char := &DnDCharacter{Level: 20}
tier := arenaGetTier(1)
monster := arenaGetMonster(1, 1)
@@ -491,9 +491,9 @@ func TestRenderArenaStreakEntry_HighLevel_AllEligible(t *testing.T) {
monster := arenaGetMonster(1, 1)
text := renderArenaStreakEntry(char, nil, tier, monster)
// Level 70 should make all tiers eligible (no 🔒)
// Level 20 (D&D cap) should make all tiers eligible (no 🔒)
if strings.Contains(text, "🔒") {
t.Error("level 70 should have all tiers eligible (no locked icons)")
t.Error("level 20 should have all tiers eligible (no locked icons)")
}
}
@@ -753,7 +753,7 @@ func TestArenaStreakSimulation_FullRun(t *testing.T) {
TierEarnings: 0,
XPAccumulated: 0,
}
combatLevel := 30
skillBonus := 20
for tierNum := 1; tierNum <= 5; tierNum++ {
tier := arenaGetTier(tierNum)
@@ -761,7 +761,7 @@ func TestArenaStreakSimulation_FullRun(t *testing.T) {
for round := 1; round <= 4; round++ {
run.Round = round
reward := arenaRoundReward(tier, round, combatLevel)
reward := arenaRoundReward(tier, round, skillBonus)
run.TierEarnings += reward
run.XPAccumulated += tier.BattleXP
run.RoundsSurvived++
@@ -791,7 +791,7 @@ func TestArenaStreakSimulation_FullRun(t *testing.T) {
for tierNum := 1; tierNum <= 5; tierNum++ {
tier := arenaGetTier(tierNum)
for round := 1; round <= 4; round++ {
rawTotal += arenaRoundReward(tier, round, combatLevel)
rawTotal += arenaRoundReward(tier, round, skillBonus)
}
rawTotal += tier.CompletionBonus
}