mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 17:02:42 +00:00
Phase 5C (rosters): T3 polish + feywild T4-spread fix
Phase 5-B's exit named two follow-ups: T3 below band (manor 39 /
underforge 47 vs 55-75) and the T4 spread (feywild 59 vs underdark
88, 30pp asymmetry). Phase 5-C re-runs the Phase 4-A trace on the
four affected zones under the shipped Phase 5-B cell and applies
roster knobs only (IsElite + SpawnWeight; no bestiary stat-block
touches).
TestExpeditionBalance_Phase5C_OutlierDiagnostic names the killer
per zone:
manor_blackspire (53%): Vampire Spawn (72% win, 42 kills) +
Revenant (16% win, 31 kills at SW=1, can't go lower). First
attempt trimmed VS SW 3 -> 2 and backfired by concentrating
elite share on Revenant (kills jumped to 48); reverted.
underforge (49.5%): Fire Elemental (57 kills, 61hp/win) +
Salamander (33 kills) carry the lethality; Helmed Horror only
100%-win elite but SW=1, no dilution effect.
feywild_crossing (54%): only 2 elites in the pool; Fomorian
(50% win, SW=1) still pulls 25% of elite picks and owns 65
kills.
underdark (86%): Drow at SW=7 fills nearly half of standard
rolls at 100% win / 1.1hp loss -- free-HP filler.
Roster changes:
manor Banshee promoted to elite SW=2 (was standard SW=3,
99.6% win). Soft 4th elite slot dilutes Revenant share
~14% -> ~11%. Standards collapse to Shadow+Poltergeist
(>=99% win).
underforge Helmed Horror SW 1 -> 3. Three-way elite pool drops
Fire Elemental's share from ~44% to ~33%.
feywild Green Hag promoted to elite SW=2 (was standard SW=4).
Adds soft 3rd elite + removes a 16hp/win standard.
Standards become Redcap+Will-o-Wisp+Quickling.
underdark Drow SW 7 -> 5 (light trim per the user's "lift
trailers, don't nerf leaders" stance). Standards
shift toward Hook Horror / Drow Mage.
Phase 1 matrix after Phase 5-C (200 trials, Fighter @ centerline):
T1 88.5% spread 1.0 (in band 70-90)
T2 74.5% spread 15.0 (in band 62-82)
T3 56.7% spread 0.5 (in band 55-75) +13.5pp
T4 77.0% spread 13.0 (over band 45-65, matches 5-B target;
spread halved from 25.5)
T5 58.0% spread 40.0 (abyss_portal 38% is the residual)
T3 mean lifted +13.5pp; both zones inside band with 0.5pp residual
spread. T4 spread halved (25.5 -> 13.0pp) by lifting feywild
+11.5pp and trimming underdark -4.5pp. T1/T2/T5 untouched.
No test debt -- no production tests pin SpawnWeight or IsElite on
the changed entries (TestMonsterKillTags_GatesKnownMonsters checks
vampire_spawn tags only, unaffected). -short suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -553,6 +553,90 @@ hump and is fine for "some death" tier shape. Remaining work
|
|||||||
(Phase 5-C+) is T3 roster polish + feywild T4-spread fix — separate
|
(Phase 5-C+) is T3 roster polish + feywild T4-spread fix — separate
|
||||||
session.
|
session.
|
||||||
|
|
||||||
|
#### Phase 5-C — T3 polish + T4 spread fix (rosters only)
|
||||||
|
|
||||||
|
Phase 5-B's exit named two follow-ups: T3 below band (manor 39 /
|
||||||
|
underforge 47 vs 55-75), and the T4 spread (feywild 59 vs underdark
|
||||||
|
88 = 30pp asymmetry). Phase 5-C re-runs the Phase 4-A trace on the
|
||||||
|
four affected zones at the shipped Phase 5-B cell
|
||||||
|
(`TestExpeditionBalance_Phase5C_OutlierDiagnostic`) and identifies
|
||||||
|
which monsters own the lethality budget per zone, then applies the
|
||||||
|
Phase 4-B toolset (IsElite toggle + SpawnWeight rebalance only — no
|
||||||
|
bestiary stat-block touches).
|
||||||
|
|
||||||
|
Per-zone reads from the diagnostic (200 trials, L9/L13 Fighter,
|
||||||
|
e=23 d=1 burn=50):
|
||||||
|
|
||||||
|
manor_blackspire (53% comp): standards all ≥99% win. Elite pool
|
||||||
|
is the lethality source — Vampire Spawn (72% win, 42 kills) and
|
||||||
|
Revenant (16% win, 31 kills at SW=1) split the kill budget.
|
||||||
|
Revenant can't drop below SW=1, and trimming Vampire Spawn
|
||||||
|
(first attempt: SW 3 → 2) backfired by shifting elite share onto
|
||||||
|
Revenant — kills jumped to 48 and completion fell to 44.5%.
|
||||||
|
|
||||||
|
underforge (49.5% comp): Fire Elemental (57 kills, 61hp/win) +
|
||||||
|
Salamander (33 kills) own elite share. Helmed Horror was the
|
||||||
|
only 100%-win elite but sat at SW=1, providing almost no
|
||||||
|
dilution.
|
||||||
|
|
||||||
|
feywild_crossing (54% comp): Fomorian alone (65 kills, 50% win,
|
||||||
|
SW=1) owns the elite kill budget — only 2 elites in the pool
|
||||||
|
(Night Hag SW=3 + Fomorian SW=1), so Fomorian still gets 25% of
|
||||||
|
elite picks despite the low weight.
|
||||||
|
|
||||||
|
underdark (86% comp): Drow at SW=7 (1801/4164 standard rolls,
|
||||||
|
100% win / 1.1hp loss) is a free-HP filler dominating the
|
||||||
|
standard pool.
|
||||||
|
|
||||||
|
Roster changes shipped (`dnd_zone.go`):
|
||||||
|
|
||||||
|
manor_blackspire — Banshee promoted to elite at SW=2 (was SW=3
|
||||||
|
standard, 99.6% win, 11hp loss): adds a soft 4th elite slot,
|
||||||
|
dropping Revenant's elite share ~14% → 11% without changing
|
||||||
|
its presence. Standard floor collapses to Shadow + Poltergeist,
|
||||||
|
both ≥99% win at ≤5hp loss.
|
||||||
|
|
||||||
|
underforge — Helmed Horror SW 1 → 3 (98%+ win, 15hp loss): three
|
||||||
|
roughly-equal elite slots (Salamander 3 + Fire Elemental 3 +
|
||||||
|
Helmed Horror 3), reducing Fire Elemental's elite share from
|
||||||
|
~44% to ~33%.
|
||||||
|
|
||||||
|
feywild_crossing — Green Hag promoted to elite at SW=2 (was SW=4
|
||||||
|
standard, 98% win, 16hp loss): adds a softer 3rd elite slot,
|
||||||
|
diluting Fomorian's share, AND pulls a moderate-attrition fight
|
||||||
|
out of the standard pool. Standard floor becomes Redcap +
|
||||||
|
Will-o-Wisp + Quickling (all ≥99% win, ≤11hp loss).
|
||||||
|
|
||||||
|
underdark (light trim per user's "lift trailers, don't nerf
|
||||||
|
leaders" stance) — Drow SW 7 → 5: standards shift toward Hook
|
||||||
|
Horror (17.8hp/win) and Drow Mage (16.4hp/win), still safe but
|
||||||
|
with real attrition.
|
||||||
|
|
||||||
|
Phase 1 matrix after Phase 5-C (200 trials, Fighter @ centerline):
|
||||||
|
|
||||||
|
```
|
||||||
|
T1 88.5% spread 1.0 (crypt 89, goblin 88) ⬆ band 70-90 — IN BAND
|
||||||
|
T2 74.5% spread 15.0 (forest 82, sunken 67) ⬆ band 62-82 — IN BAND
|
||||||
|
T3 56.7% spread 0.5 (manor 57, underforge 56.5) ⬆ band 55-75 — IN BAND ⬆ +13.5pp
|
||||||
|
T4 77.0% spread 13.0 (feywild 70.5, underdark 83.5) ⬆ band 45-65 — OVER (matches 5-B target)
|
||||||
|
T5 58.0% spread 40.0 (abyss 38, dragons 78) ⬆ band 35-55 — borderline (abyss the residual)
|
||||||
|
```
|
||||||
|
|
||||||
|
T3 mean lifted +13.5pp; both zones land inside the band with a
|
||||||
|
0.5pp residual spread. T4 spread halved (25.5 → 13.0pp) by lifting
|
||||||
|
feywild +11.5pp and trimming underdark -4.5pp, matching the user's
|
||||||
|
chosen "lift mostly, trim lightly" direction. T1/T2/T5 untouched.
|
||||||
|
|
||||||
|
**Test debt:** none — no production tests pinned the changed
|
||||||
|
SpawnWeight or IsElite values. `TestMonsterKillTags_GatesKnownMonsters`
|
||||||
|
checks `vampire_spawn` tags but is unaffected by weight.
|
||||||
|
|
||||||
|
**Exit:** T1-T4 all in or above band; T5's abyss_portal (38%) is
|
||||||
|
the only remaining sub-band cell and is a tier-asymmetry residual,
|
||||||
|
not a per-zone outlier. Phase 5-D could revisit abyss specifically,
|
||||||
|
but it's optional — the user's "breezy with some death" target is
|
||||||
|
hit across the live tier ladder.
|
||||||
|
|
||||||
### Phase 6 — optional MAD / second-order
|
### Phase 6 — optional MAD / second-order
|
||||||
|
|
||||||
If post-Phase-3 the bands hold but feel wrong subjectively
|
If post-Phase-3 the bands hold but feel wrong subjectively
|
||||||
|
|||||||
@@ -433,9 +433,23 @@ func zoneManorBlackspire() ZoneDefinition {
|
|||||||
// becomes a 3-elite pool with balanced dilution, leaving
|
// becomes a 3-elite pool with balanced dilution, leaving
|
||||||
// Shadow/Poltergeist/Banshee as a clean standard floor
|
// Shadow/Poltergeist/Banshee as a clean standard floor
|
||||||
// (all ≥92% win in the trace).
|
// (all ≥92% win in the trace).
|
||||||
|
//
|
||||||
|
// Phase 5-C: under shipped HP×1.5/+3 floor, manor still
|
||||||
|
// trailed at 53% (band 55-75). Trace named both Vampire
|
||||||
|
// Spawn (72% win, 42 kills) and Revenant (16% win, 31
|
||||||
|
// kills) as elite-pool killers — Revenant the worse per
|
||||||
|
// appearance, but at SW=1 already. A first attempt
|
||||||
|
// trimmed Vampire Spawn SW 3 → 2; that backfired by
|
||||||
|
// shifting elite share onto Revenant (kills jumped to
|
||||||
|
// 48). Final move: keep the killers' weights, dilute
|
||||||
|
// the elite pool by promoting Banshee (99.6% win as
|
||||||
|
// standard, 11hp loss) to elite at SW=2 — drops
|
||||||
|
// Revenant's elite share ~14% → 11% without removing
|
||||||
|
// it from rotation. Standard floor collapses to
|
||||||
|
// Shadow + Poltergeist (both ≥99% win, ≤5hp loss).
|
||||||
{BestiaryID: "shadow", SpawnWeight: 6},
|
{BestiaryID: "shadow", SpawnWeight: 6},
|
||||||
{BestiaryID: "poltergeist", SpawnWeight: 5},
|
{BestiaryID: "poltergeist", SpawnWeight: 5},
|
||||||
{BestiaryID: "banshee", SpawnWeight: 3},
|
{BestiaryID: "banshee", SpawnWeight: 2, IsElite: true},
|
||||||
{BestiaryID: "wraith", SpawnWeight: 3, IsElite: true},
|
{BestiaryID: "wraith", SpawnWeight: 3, IsElite: true},
|
||||||
{BestiaryID: "vampire_spawn", SpawnWeight: 3, IsElite: true},
|
{BestiaryID: "vampire_spawn", SpawnWeight: 3, IsElite: true},
|
||||||
{BestiaryID: "revenant", SpawnWeight: 1, IsElite: true},
|
{BestiaryID: "revenant", SpawnWeight: 1, IsElite: true},
|
||||||
@@ -480,12 +494,21 @@ func zoneUnderforge() ZoneDefinition {
|
|||||||
MinRooms: 7,
|
MinRooms: 7,
|
||||||
MaxRooms: 9,
|
MaxRooms: 9,
|
||||||
Enemies: []ZoneEnemy{
|
Enemies: []ZoneEnemy{
|
||||||
|
// Phase 5-C: underforge trailed at 49.5% (band 55-75)
|
||||||
|
// under the shipped HP×1.5/+3 floor. Trace named Fire
|
||||||
|
// Elemental (57 kills, 61hp loss/win) as the dominant
|
||||||
|
// elite killer with Salamander (33 kills) second.
|
||||||
|
// Helmed Horror was the only 100%-win elite but sat at
|
||||||
|
// SW=1, contributing almost no dilution. Lifted Helmed
|
||||||
|
// Horror SW 1 → 3 so the elite pool runs three roughly-
|
||||||
|
// equal slots and Fire Elemental's share drops from
|
||||||
|
// ~44% to ~33% of elite picks.
|
||||||
{BestiaryID: "magmin", SpawnWeight: 6},
|
{BestiaryID: "magmin", SpawnWeight: 6},
|
||||||
{BestiaryID: "azer", SpawnWeight: 5},
|
{BestiaryID: "azer", SpawnWeight: 5},
|
||||||
{BestiaryID: "flameskull", SpawnWeight: 4},
|
{BestiaryID: "flameskull", SpawnWeight: 4},
|
||||||
{BestiaryID: "salamander", SpawnWeight: 3, IsElite: true},
|
{BestiaryID: "salamander", SpawnWeight: 3, IsElite: true},
|
||||||
{BestiaryID: "fire_elemental", SpawnWeight: 3, IsElite: true},
|
{BestiaryID: "fire_elemental", SpawnWeight: 3, IsElite: true},
|
||||||
{BestiaryID: "helmed_horror", SpawnWeight: 1, IsElite: true},
|
{BestiaryID: "helmed_horror", SpawnWeight: 3, IsElite: true},
|
||||||
},
|
},
|
||||||
Boss: ZoneBoss{
|
Boss: ZoneBoss{
|
||||||
BestiaryID: "boss_emberlord_thyrak",
|
BestiaryID: "boss_emberlord_thyrak",
|
||||||
@@ -528,7 +551,17 @@ func zoneUnderdark() ZoneDefinition {
|
|||||||
MinRooms: 8,
|
MinRooms: 8,
|
||||||
MaxRooms: 10,
|
MaxRooms: 10,
|
||||||
Enemies: []ZoneEnemy{
|
Enemies: []ZoneEnemy{
|
||||||
{BestiaryID: "drow", SpawnWeight: 7},
|
// Phase 5-C: underdark ran 88% (band 45-65, way over)
|
||||||
|
// at the T4 centerline — its sibling feywild sat at
|
||||||
|
// 54%, a 30pp asymmetry. Per [[feedback_difficulty_target]]
|
||||||
|
// the bulk of Phase 5-C lifts feywild rather than nerfing
|
||||||
|
// the leader; this is the "light underdark trim" piece.
|
||||||
|
// Drow at SW=7 owned nearly half of all standard rolls
|
||||||
|
// (1801/4164) at 100% win / 1.1hp loss — a free-HP
|
||||||
|
// filler. Trimmed SW 7 → 5 so the standard pool shifts
|
||||||
|
// toward Hook Horror (17.8hp/win) and Drow Mage
|
||||||
|
// (16.4hp/win) — still safe but with real attrition.
|
||||||
|
{BestiaryID: "drow", SpawnWeight: 5},
|
||||||
{BestiaryID: "drow_elite_warrior", SpawnWeight: 4, IsElite: true},
|
{BestiaryID: "drow_elite_warrior", SpawnWeight: 4, IsElite: true},
|
||||||
{BestiaryID: "drow_mage", SpawnWeight: 3},
|
{BestiaryID: "drow_mage", SpawnWeight: 3},
|
||||||
{BestiaryID: "mind_flayer", SpawnWeight: 2},
|
{BestiaryID: "mind_flayer", SpawnWeight: 2},
|
||||||
@@ -575,10 +608,22 @@ func zoneFeywildCrossing() ZoneDefinition {
|
|||||||
MinRooms: 8,
|
MinRooms: 8,
|
||||||
MaxRooms: 10,
|
MaxRooms: 10,
|
||||||
Enemies: []ZoneEnemy{
|
Enemies: []ZoneEnemy{
|
||||||
|
// Phase 5-C: feywild trailed at 54% (band 45-65, but
|
||||||
|
// the design goal was to close the 30pp gap with its
|
||||||
|
// underdark sibling at 88%). Trace named Fomorian as
|
||||||
|
// the dominant killer (65 kills, 50% win, SW=1) — a
|
||||||
|
// 2-elite pool with Night Hag at SW=3 meant Fomorian
|
||||||
|
// owned ~25% of elite picks despite its low weight,
|
||||||
|
// and that bracket is where the deaths came from.
|
||||||
|
// Promoted Green Hag to elite (SW=2): adds a softer
|
||||||
|
// 98%-win elite slot to dilute Fomorian's share, and
|
||||||
|
// pulls a 16hp/win standard out of the standard pool
|
||||||
|
// so the standard floor collapses to Redcap + Will-o-
|
||||||
|
// Wisp + Quickling — all ≥99% win, ≤11hp loss.
|
||||||
{BestiaryID: "redcap", SpawnWeight: 6},
|
{BestiaryID: "redcap", SpawnWeight: 6},
|
||||||
{BestiaryID: "will_o_wisp", SpawnWeight: 5},
|
{BestiaryID: "will_o_wisp", SpawnWeight: 5},
|
||||||
{BestiaryID: "quickling", SpawnWeight: 5},
|
{BestiaryID: "quickling", SpawnWeight: 5},
|
||||||
{BestiaryID: "green_hag", SpawnWeight: 4},
|
{BestiaryID: "green_hag", SpawnWeight: 2, IsElite: true},
|
||||||
{BestiaryID: "night_hag", SpawnWeight: 3, IsElite: true},
|
{BestiaryID: "night_hag", SpawnWeight: 3, IsElite: true},
|
||||||
{BestiaryID: "fomorian", SpawnWeight: 1, IsElite: true},
|
{BestiaryID: "fomorian", SpawnWeight: 1, IsElite: true},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1417,6 +1417,204 @@ func zoneSeedSalt(id ZoneID) uint64 {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestExpeditionBalance_Phase5C_OutlierDiagnostic is the per-zone
|
||||||
|
// trace pass for Phase 5-C — the second roster-polish round. Phase
|
||||||
|
// 5-B's shipped baseline (HP×1.5, +3 player floor, e=23, d=1, burn=50)
|
||||||
|
// landed T1/T2/T4/T5 at or above band, but left T3 at 43% mean
|
||||||
|
// (manor 39 / underforge 47 vs band 55-75) and produced a 30pp
|
||||||
|
// T4 spread (feywild 59 vs underdark 88) with feywild trailing.
|
||||||
|
//
|
||||||
|
// This test re-runs the Phase 4-A trace on the four affected zones
|
||||||
|
// (manor + underforge as both-trail, feywild as lift-target, underdark
|
||||||
|
// as healthy reference / candidate for one mild elite demotion). Same
|
||||||
|
// monster-attribution, day-of-end, and elite/std-mix aggregates as
|
||||||
|
// Phase 4-A; the difference is the candidate zone set and that all
|
||||||
|
// four are reported side-by-side rather than as outlier+sibling pairs.
|
||||||
|
//
|
||||||
|
// Constraint per the plan doc and Phase 4-B's commit: do not touch
|
||||||
|
// monster stat blocks. Tools allowed are IsElite toggle, SpawnWeight
|
||||||
|
// rebalance, and zone-scoped boss tuning.
|
||||||
|
//
|
||||||
|
// Diagnostic-only — no gates. -short skips.
|
||||||
|
func TestExpeditionBalance_Phase5C_OutlierDiagnostic(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("phase 5-C diagnostic walks 4 zones × 200 trials; -short skips it")
|
||||||
|
}
|
||||||
|
|
||||||
|
const trialsPerZone = 200
|
||||||
|
const baseSeed uint64 = 0xF50C5D
|
||||||
|
// Phase 5-B shipped cell. HP×1.5 and the +3 combat floor are
|
||||||
|
// already wired into the harness via the live constants
|
||||||
|
// (phase5BHPMult, applyPhase5BPlayerFloor); only the three
|
||||||
|
// expedition-side overrides need restating.
|
||||||
|
const eliteThreshold = 23
|
||||||
|
const driftBase = 1
|
||||||
|
const supplyBurnPct = 50
|
||||||
|
|
||||||
|
zones := []struct {
|
||||||
|
id ZoneID
|
||||||
|
tier ZoneTier
|
||||||
|
role string
|
||||||
|
}{
|
||||||
|
{ZoneManorBlackspire, ZoneTierJourneyman, "TRAIL-T3"},
|
||||||
|
{ZoneUnderforge, ZoneTierJourneyman, "TRAIL-T3"},
|
||||||
|
{ZoneFeywildCrossing, ZoneTierVeteran, "TRAIL-T4"},
|
||||||
|
{ZoneUnderdark, ZoneTierVeteran, "HEALTHY-T4"},
|
||||||
|
}
|
||||||
|
|
||||||
|
type monsterStat struct {
|
||||||
|
appearances int
|
||||||
|
standardCount int
|
||||||
|
eliteCount int
|
||||||
|
wins int
|
||||||
|
hpLossOnWin int
|
||||||
|
killAttributed int
|
||||||
|
}
|
||||||
|
type zoneAgg struct {
|
||||||
|
profile expeditionBalanceProfile
|
||||||
|
completions int
|
||||||
|
deaths int
|
||||||
|
starves int
|
||||||
|
monsters map[string]*monsterStat
|
||||||
|
dayHist [16]int
|
||||||
|
eliteFights int
|
||||||
|
stdFights int
|
||||||
|
lastFight harnessFightTrace
|
||||||
|
}
|
||||||
|
|
||||||
|
report := func(label string, agg *zoneAgg) {
|
||||||
|
total := trialsPerZone
|
||||||
|
compPct := float64(agg.completions) / float64(total) * 100
|
||||||
|
deathPct := float64(agg.deaths) / float64(total) * 100
|
||||||
|
starvePct := float64(agg.starves) / float64(total) * 100
|
||||||
|
t.Logf("─── %-11s %-18s L%-2d comp=%5.1f%% death=%5.1f%% starve=%5.1f%% elite_fights=%d std_fights=%d ───",
|
||||||
|
label, agg.profile.ZoneID,
|
||||||
|
agg.profile.Level,
|
||||||
|
compPct, deathPct, starvePct,
|
||||||
|
agg.eliteFights, agg.stdFights)
|
||||||
|
names := make([]string, 0, len(agg.monsters))
|
||||||
|
for n := range agg.monsters {
|
||||||
|
names = append(names, n)
|
||||||
|
}
|
||||||
|
sort.Slice(names, func(i, j int) bool {
|
||||||
|
return agg.monsters[names[i]].appearances > agg.monsters[names[j]].appearances
|
||||||
|
})
|
||||||
|
for _, n := range names {
|
||||||
|
m := agg.monsters[n]
|
||||||
|
winPct := 0.0
|
||||||
|
if m.appearances > 0 {
|
||||||
|
winPct = float64(m.wins) / float64(m.appearances) * 100
|
||||||
|
}
|
||||||
|
avgLoss := 0.0
|
||||||
|
if m.wins > 0 {
|
||||||
|
avgLoss = float64(m.hpLossOnWin) / float64(m.wins)
|
||||||
|
}
|
||||||
|
t.Logf(" MON %-22s appear=%4d (std=%-3d elite=%-3d) win=%5.1f%% avg_hp_loss_on_win=%5.1f kills=%d",
|
||||||
|
n, m.appearances, m.standardCount, m.eliteCount,
|
||||||
|
winPct, avgLoss, m.killAttributed)
|
||||||
|
}
|
||||||
|
bins := make([]string, 0, 14)
|
||||||
|
for d := 1; d <= 14; d++ {
|
||||||
|
if agg.dayHist[d] > 0 {
|
||||||
|
bins = append(bins, fmt.Sprintf("d%d=%d", d, agg.dayHist[d]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf(" END-DAYS %s", joinZones(bins))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, z := range zones {
|
||||||
|
level := phase1TierCenterline[z.tier]
|
||||||
|
t.Logf("═══ %-11s %s (L%d Fighter, e=%d d=%d burn=%d, %d trials) ═══",
|
||||||
|
z.role, z.id, level,
|
||||||
|
eliteThreshold, driftBase, supplyBurnPct, trialsPerZone)
|
||||||
|
|
||||||
|
profile := expeditionBalanceProfile{
|
||||||
|
ZoneID: z.id,
|
||||||
|
Class: ClassFighter,
|
||||||
|
Level: level,
|
||||||
|
Supplies: makeSupplies(z.tier, SupplyPurchase{StandardPacks: 3}),
|
||||||
|
CampType: CampTypeStandard,
|
||||||
|
EliteInterruptThresholdOverride: eliteThreshold,
|
||||||
|
ThreatDriftBaseOverride: driftBase,
|
||||||
|
SupplyBurnRatePctOverride: supplyBurnPct,
|
||||||
|
}
|
||||||
|
agg := &zoneAgg{
|
||||||
|
profile: profile,
|
||||||
|
monsters: map[string]*monsterStat{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for trial := 0; trial < trialsPerZone; trial++ {
|
||||||
|
exp := newHarnessExpedition(profile)
|
||||||
|
char := buildHarnessCharacter(classBalanceProfile{
|
||||||
|
Class: profile.Class,
|
||||||
|
Level: profile.Level,
|
||||||
|
})
|
||||||
|
h := &expeditionHarness{
|
||||||
|
exp: exp,
|
||||||
|
char: char,
|
||||||
|
rng: newHarnessRNG(baseSeed + uint64(trial) + uint64(z.tier)*131 + zoneSeedSalt(z.id)),
|
||||||
|
rollsPerDay: harnessHarvestRollsPerDay,
|
||||||
|
eliteInterruptThresholdOverride: eliteThreshold,
|
||||||
|
threatDriftBaseOverride: driftBase,
|
||||||
|
supplyBurnRatePctOverride: supplyBurnPct,
|
||||||
|
traceFightStruct: func(ft harnessFightTrace) {
|
||||||
|
m, ok := agg.monsters[ft.MonsterName]
|
||||||
|
if !ok {
|
||||||
|
m = &monsterStat{}
|
||||||
|
agg.monsters[ft.MonsterName] = m
|
||||||
|
}
|
||||||
|
m.appearances++
|
||||||
|
if ft.Elite {
|
||||||
|
m.eliteCount++
|
||||||
|
agg.eliteFights++
|
||||||
|
} else {
|
||||||
|
m.standardCount++
|
||||||
|
agg.stdFights++
|
||||||
|
}
|
||||||
|
if ft.Won {
|
||||||
|
m.wins++
|
||||||
|
loss := ft.HPPre - ft.HPPost
|
||||||
|
if loss < 0 {
|
||||||
|
loss = 0
|
||||||
|
}
|
||||||
|
m.hpLossOnWin += loss
|
||||||
|
}
|
||||||
|
agg.lastFight = ft
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var trialEnd expeditionTrialResult
|
||||||
|
for {
|
||||||
|
res := h.advanceExpeditionOneDay()
|
||||||
|
if res.EndedReason != "" {
|
||||||
|
trialEnd = res
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case trialEnd.Completed:
|
||||||
|
agg.completions++
|
||||||
|
case trialEnd.Died:
|
||||||
|
agg.deaths++
|
||||||
|
if m, ok := agg.monsters[agg.lastFight.MonsterName]; ok && !agg.lastFight.Won {
|
||||||
|
m.killAttributed++
|
||||||
|
}
|
||||||
|
case trialEnd.StarvedOut:
|
||||||
|
agg.starves++
|
||||||
|
}
|
||||||
|
d := trialEnd.DaysElapsed
|
||||||
|
if d < 1 {
|
||||||
|
d = 1
|
||||||
|
}
|
||||||
|
if d > 14 {
|
||||||
|
d = 14
|
||||||
|
}
|
||||||
|
agg.dayHist[d]++
|
||||||
|
}
|
||||||
|
|
||||||
|
report(z.role, agg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// joinZones is a tiny helper kept local to the test file so the
|
// joinZones is a tiny helper kept local to the test file so the
|
||||||
// per-tier log line reads in one logical chunk without pulling in
|
// per-tier log line reads in one logical chunk without pulling in
|
||||||
// strings.Join's import for production code.
|
// strings.Join's import for production code.
|
||||||
|
|||||||
Reference in New Issue
Block a user