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
|
// The per-anchor chances below put a player who hits all three at ~1 event
|
||||||
// per week; see TestAnchoredEventWeeklyRate.
|
// 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 (
|
const (
|
||||||
advEventChanceDigest = 0.08
|
advEventChanceDigest = 0.08
|
||||||
advEventChanceSell = 0.05
|
advEventChanceSell = 0.05
|
||||||
advEventChanceArena = 0.05
|
advEventChanceArena = 0.05
|
||||||
|
advEventChanceZoneClear = 0.08
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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.
|
// TestClaimDailyEventSlot_OnePerDay guards the cap the rate test assumes.
|
||||||
func TestClaimDailyEventSlot_OnePerDay(t *testing.T) {
|
func TestClaimDailyEventSlot_OnePerDay(t *testing.T) {
|
||||||
uid := id.UserID("@events-slot:example")
|
uid := id.UserID("@events-slot:example")
|
||||||
|
|||||||
@@ -471,6 +471,12 @@ type advanceResult struct {
|
|||||||
// harvest ran (Entry/Trap/Elite/Boss rooms, forks, stops).
|
// harvest ran (Entry/Trap/Elite/Boss rooms, forks, stops).
|
||||||
harvest autoHarvestResult
|
harvest autoHarvestResult
|
||||||
harvestFooter string
|
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,
|
// zoneCmdAdvance resolves the room the player is currently standing in,
|
||||||
@@ -495,7 +501,16 @@ func (p *AdventurePlugin) zoneCmdAdvance(ctx MessageContext) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendDM(ctx.Sender, err.Error())
|
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
|
// 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
|
// and point at the next one — "Cleared {zone}. Run complete." reads
|
||||||
// wrong right before the auto-advance transit block (and is shared with
|
// wrong right before the auto-advance transit block (and is shared with
|
||||||
// manual `!region travel`, which advances next).
|
// 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))
|
b.WriteString(fmt.Sprintf("🏁 **Cleared %s.** The way to %s opens ahead.\n\n", region.Name, next.Name))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(fmt.Sprintf("🏆 **Cleared %s.** Run complete.\n\n", zone.Display))
|
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,
|
phases: phases,
|
||||||
final: b.String(),
|
final: b.String(),
|
||||||
reason: stopComplete,
|
reason: stopComplete,
|
||||||
|
zoneCleared: !midZone,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if forkMsg != "" {
|
if forkMsg != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user