mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Review follow-up K: a fourth event anchor for the grind loop
The three A6 presence anchors (expedition Night digest, !sell, arena cashout) miss a whole playstyle: a player who only mines/forages/fishes (now automatic, done by walking) and clears single-day dungeons, but never sells, never enters the arena, and never runs a multi-day expedition to a Night camp, would roll for zero mid-day events. Anchor a fourth roll on a foreground single-day zone clear — the climax DM that player is reading, and one they cannot spam (a clear costs a full walk). advanceResult.zoneCleared is set only in the full-clear branch (a mid-zone region clear continues the run and would fire per region), and the roll lives in zoneCmdAdvance, the foreground path only: autopilot goes through runAutopilotWalk and party members are refused earlier by isPartyMember. The per-day slot guard still caps everyone at one event/day, so the three prior anchors are unmoved. advEventChanceZoneClear = 0.08 puts a zone-clear-only player at ~1 event/week, matching the fully-engaged player; it is the tuning knob. Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
@@ -32,10 +32,23 @@ type advActiveEvent struct {
|
||||
//
|
||||
// The per-anchor chances below put a player who hits all three at ~1 event
|
||||
// per week; see TestAnchoredEventWeeklyRate.
|
||||
//
|
||||
// K — the three social/economic anchors miss the pure grind loop: a player who
|
||||
// only mines/forages/fishes (now automatic, done by walking) and clears
|
||||
// single-day dungeons, but never sells, never enters the arena, and never runs
|
||||
// a multi-day expedition to a Night camp, would roll for zero events. The
|
||||
// fourth anchor, ZoneClear, fires on a foreground single-day zone clear — the
|
||||
// climax DM that player *is* reading — so they get the same rough cadence.
|
||||
// Autopilot walks and mid-zone region clears deliberately do not fire it (see
|
||||
// zoneCmdAdvance / the full-clear branch of advanceOnceWithOpts). The per-day
|
||||
// slot still caps everyone at one event, so a player who happens to hit two
|
||||
// anchors in a day can't double up. This rate is the tuning knob for item K —
|
||||
// bump it if telemetry shows the grind-loop player still sees too few.
|
||||
const (
|
||||
advEventChanceDigest = 0.08
|
||||
advEventChanceSell = 0.05
|
||||
advEventChanceArena = 0.05
|
||||
advEventChanceZoneClear = 0.08
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -44,6 +44,43 @@ func TestAnchoredEventWeeklyRate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestZoneClearAnchorRate pins item K's fourth anchor for the population the
|
||||
// other three miss: a grind-loop player who never sells, never enters the
|
||||
// arena, and never runs a multi-day expedition, but clears single-day zones.
|
||||
// Modelled at two foreground clears per active day (a plausible dungeon
|
||||
// session) they should land in the same ~1 event/week band as the fully-
|
||||
// engaged player — that is the whole point of adding the anchor. The clears/day
|
||||
// assumption and advEventChanceZoneClear are the two tuning knobs; see the K
|
||||
// note in adventure_events.go.
|
||||
func TestZoneClearAnchorRate(t *testing.T) {
|
||||
const clearsPerDay = 2
|
||||
|
||||
const weeks = 20000
|
||||
rng := rand.New(rand.NewPCG(0x5EED, 0x2C)) // "ZC"
|
||||
events := 0
|
||||
for range weeks {
|
||||
for range 7 {
|
||||
firedToday := false
|
||||
for range clearsPerDay {
|
||||
if firedToday {
|
||||
break // one event per player per UTC day
|
||||
}
|
||||
if rng.Float64() < advEventChanceZoneClear {
|
||||
firedToday = true
|
||||
}
|
||||
}
|
||||
if firedToday {
|
||||
events++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
perWeek := float64(events) / weeks
|
||||
if perWeek < 0.8 || perWeek > 1.5 {
|
||||
t.Errorf("zone-clear anchor = %.3f/week, want ~1 (0.8–1.5)", perWeek)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClaimDailyEventSlot_OnePerDay guards the cap the rate test assumes.
|
||||
func TestClaimDailyEventSlot_OnePerDay(t *testing.T) {
|
||||
uid := id.UserID("@events-slot:example")
|
||||
|
||||
@@ -471,6 +471,12 @@ type advanceResult struct {
|
||||
// harvest ran (Entry/Trap/Elite/Boss rooms, forks, stops).
|
||||
harvest autoHarvestResult
|
||||
harvestFooter string
|
||||
// zoneCleared is true only when this step was a *full* zone clear
|
||||
// (reason == stopComplete and not a mid-zone region clear). The K
|
||||
// event anchor reads it so a foreground `!zone advance` clear can roll
|
||||
// the grind-loop player's mid-day event — a mid-zone region clear does
|
||||
// not, since the run continues and it would fire per region.
|
||||
zoneCleared bool
|
||||
}
|
||||
|
||||
// zoneCmdAdvance resolves the room the player is currently standing in,
|
||||
@@ -495,7 +501,16 @@ func (p *AdventurePlugin) zoneCmdAdvance(ctx MessageContext) error {
|
||||
if err != nil {
|
||||
return p.SendDM(ctx.Sender, err.Error())
|
||||
}
|
||||
return p.streamOrSend(ctx.Sender, res.preStream, res.intro, res.phases, res.final)
|
||||
sendErr := p.streamOrSend(ctx.Sender, res.preStream, res.intro, res.phases, res.final)
|
||||
// K fourth event anchor: a foreground single-day zone clear is a presence
|
||||
// moment for the grind-loop player the other three anchors miss. Rolled
|
||||
// after the clear DM so any triggered event lands behind it, and only here
|
||||
// — zoneCmdAdvance is the foreground path; autopilot walks go through
|
||||
// runAutopilotWalk and never reach this.
|
||||
if res.zoneCleared {
|
||||
p.maybeFireAnchoredEvent(ctx.Sender, advEventChanceZoneClear)
|
||||
}
|
||||
return sendErr
|
||||
}
|
||||
|
||||
// advanceOnce runs the single-room advance pipeline and returns a
|
||||
@@ -737,7 +752,8 @@ func (p *AdventurePlugin) advanceOnceWithOpts(ctx MessageContext, compact, inlin
|
||||
// and point at the next one — "Cleared {zone}. Run complete." reads
|
||||
// wrong right before the auto-advance transit block (and is shared with
|
||||
// manual `!region travel`, which advances next).
|
||||
if region, next, midZone := midZoneRegionClear(ctx.Sender, run.RunID); midZone {
|
||||
region, next, midZone := midZoneRegionClear(ctx.Sender, run.RunID)
|
||||
if midZone {
|
||||
b.WriteString(fmt.Sprintf("🏁 **Cleared %s.** The way to %s opens ahead.\n\n", region.Name, next.Name))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("🏆 **Cleared %s.** Run complete.\n\n", zone.Display))
|
||||
@@ -768,6 +784,7 @@ func (p *AdventurePlugin) advanceOnceWithOpts(ctx MessageContext, compact, inlin
|
||||
phases: phases,
|
||||
final: b.String(),
|
||||
reason: stopComplete,
|
||||
zoneCleared: !midZone,
|
||||
}, nil
|
||||
}
|
||||
if forkMsg != "" {
|
||||
|
||||
Reference in New Issue
Block a user