mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
J3 D8-b: sim picker upcasting + discovery picker is dead since D3
simPickSpell now enumerates one candidate per available slot >= native
for every prepared damage spell, scored via spellExpectedDamage with the
existing "+1 die per slot" scaling. Cantrips contribute a single slot-0
candidate. Tie-break: highest slot first, then highest expDmg. Picker
returns "<id> --upcast N" when the winning slot exceeds native so
parseCombatCast upcasts in the engine. Build + plugin tests green.
Then: measured zero impact. d8b_corpus.jsonl (same 10x5x100 matrix as
d7d) lands every class within +/-1.5pp of d7d baseline.
Root cause discovered post-corpus: simPickSpell is dead code in the
current sim path. Commit 68ed8e7 ("Long expeditions D3: compact
autopilot auto-resolves boss rooms") added a !compact gate at
dnd_expedition_cmd.go:810 so compact autopilot inline-resolves boss/
elite rooms via resolveCombatRoom -> runZoneCombat -> SimulateCombat
instead of returning stopBoss/stopElite for autoResolveCombat to handle.
The sim uses compact=true (expedition_sim.go:433), so autoResolveCombat
- the only caller of simPickCombatAction/simPickSpell - never fires.
Verified empirically: a stderr-traced simPickSpell produced zero hits
across full bard/cleric L10 runs.
This rewrites the picker-era retrospective. J2's baseline_j2a_v2_all10
(bard 40%, cleric 39%) was measured before D3 with the picker live.
d7d's L10 baseline (bard 34%, cleric 21%) is post-D3 with the picker
disconnected - that 6-18pp drop is the post-D3 caster regression, not
"long-expedition mechanics didn't help casters" as d7d framed it.
D8-a's "+2.2pp cleric" was also noise (1sigma ~ 1.8pp on n=500).
D8-b code kept in tree (correct, currently inert, turns on as soon as
the wire reconnects). Plan rewritten in section 8: D8-b and D8-c are
deferred behind D8-prereq, which splits compact so the sim opts out of
compact-inline boss/elite combat without affecting prod rendering.
Files:
- internal/plugin/expedition_sim.go: simPickSpell upcast enumeration
- gogobee_long_expedition_plan.md: D8-b implemented-but-inert,
D8-prereq added as next step
- sim_results/d8b_corpus.jsonl: 5000-row corpus retained for the
picker-dead baseline
This commit is contained in:
@@ -930,8 +930,10 @@ func simPickSpiritualWeapon(c *DnDCharacter, uid id.UserID, sess *CombatSession)
|
||||
return "spiritual_weapon"
|
||||
}
|
||||
|
||||
// simPickSpell returns the spell ID a competent player would cast this
|
||||
// turn, or "" when no usable spell is available (forcing a !attack).
|
||||
// simPickSpell returns the spell argument a competent player would pass
|
||||
// to !cast this turn, or "" when no usable spell is available (forcing a
|
||||
// !attack). The return value is fed straight to handleCombatCastCmd, so
|
||||
// upcast picks come back as `"<spell_id> --upcast N"`.
|
||||
// Selection rules:
|
||||
// - Only damage-effect spells (damage_attack / damage_save / damage_auto).
|
||||
// Control/buff/heal are out (J2c sweep showed control scoring at
|
||||
@@ -939,12 +941,13 @@ func simPickSpiritualWeapon(c *DnDCharacter, uid id.UserID, sess *CombatSession)
|
||||
// no headroom worth the complexity). Healing is handled by the
|
||||
// consumable-first branch in simPickCombatAction.
|
||||
// - Reaction-cast spells are excluded (engine rejects them).
|
||||
// - Non-cantrips require an available slot at their native level (no
|
||||
// upcasting — preserves high slots for high-level spells).
|
||||
// - Among feasible candidates, prefer higher slot level; tie-break on
|
||||
// - For each prepared leveled spell, enumerate one candidate per
|
||||
// available slot at level ≥ native (D8-b, aggressive upcasting).
|
||||
// spellExpectedDamage handles the +1-die-per-slot-above-native
|
||||
// scaling. Cantrips contribute one slot-0 candidate.
|
||||
// - Among feasible candidates, prefer higher slot level (preserves
|
||||
// high-slot supremacy and burns the big slots first); tie-break on
|
||||
// expected damage from the dice string.
|
||||
//
|
||||
// Returns the spell ID for handleCombatCastCmd.
|
||||
func simPickSpell(c *DnDCharacter, uid id.UserID) string {
|
||||
known, err := listKnownSpells(uid)
|
||||
if err != nil || len(known) == 0 {
|
||||
@@ -952,9 +955,10 @@ func simPickSpell(c *DnDCharacter, uid id.UserID) string {
|
||||
}
|
||||
slots, _ := getSpellSlots(uid)
|
||||
type cand struct {
|
||||
id string
|
||||
level int
|
||||
expDmg float64
|
||||
id string
|
||||
slot int
|
||||
nativeLevel int
|
||||
expDmg float64
|
||||
}
|
||||
var cands []cand
|
||||
for _, k := range known {
|
||||
@@ -983,24 +987,40 @@ func simPickSpell(c *DnDCharacter, uid id.UserID) string {
|
||||
if !onList {
|
||||
continue
|
||||
}
|
||||
if sp.Level > 0 {
|
||||
pair, ok := slots[sp.Level]
|
||||
if sp.Level == 0 {
|
||||
cands = append(cands, cand{id: sp.ID, slot: 0, nativeLevel: 0, expDmg: spellExpectedDamage(sp, 0, c.Level)})
|
||||
continue
|
||||
}
|
||||
// simMaxSlot mirrors parseCombatCast's slot-level cap; anything
|
||||
// above it would be rejected by the cast handler anyway.
|
||||
const simMaxSlot = 5
|
||||
for sl := sp.Level; sl <= simMaxSlot; sl++ {
|
||||
pair, ok := slots[sl]
|
||||
if !ok || pair[0]-pair[1] <= 0 {
|
||||
continue
|
||||
}
|
||||
cands = append(cands, cand{
|
||||
id: sp.ID,
|
||||
slot: sl,
|
||||
nativeLevel: sp.Level,
|
||||
expDmg: spellExpectedDamage(sp, sl, c.Level),
|
||||
})
|
||||
}
|
||||
cands = append(cands, cand{id: sp.ID, level: sp.Level, expDmg: spellExpectedDamage(sp, sp.Level, c.Level)})
|
||||
}
|
||||
if len(cands) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Slice(cands, func(i, j int) bool {
|
||||
if cands[i].level != cands[j].level {
|
||||
return cands[i].level > cands[j].level
|
||||
if cands[i].slot != cands[j].slot {
|
||||
return cands[i].slot > cands[j].slot
|
||||
}
|
||||
return cands[i].expDmg > cands[j].expDmg
|
||||
})
|
||||
return cands[0].id
|
||||
best := cands[0]
|
||||
if best.slot > best.nativeLevel && best.nativeLevel > 0 {
|
||||
return fmt.Sprintf("%s --upcast %d", best.id, best.slot)
|
||||
}
|
||||
return best.id
|
||||
}
|
||||
|
||||
// applyAutoCamp drives the production camp scheduler under the sim's
|
||||
|
||||
Reference in New Issue
Block a user