mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 17:02:42 +00:00
boredom: the idle clock has to be seeded, or the deploy empties the town
last_player_action_at is NULL for every existing row on the deploy that adds it, and loadBoredomCandidates COALESCEs the NULL onto created_at. created_at is account age, not an idle clock: for anyone who has been playing for a month it is a month old. So the first tick after restart would read the entire server as idle-since-creation and march all of them into a dungeon thirty minutes later, coins debited, including the player who typed a command a minute before the deploy. Seed the column once from last_active_at instead — the best "did something recently" proxy that exists at deploy time, recent for the regulars and stale for the lapsed. It stays unusable as the clock itself (the autopilot bumps it), which is why this is a one-time seed and not a fallback in the query. Re-running on every boot is a no-op, and it skips rows that already have last_boredom_at set, so a restart mid-boredom can't hand a bored character a fresh idle clock off its own autopilot writes and un-bore it permanently. Two smaller ones in the same clock: - a pending prompt counted as an action regardless of ExpiresAt, and nothing sweeps p.pending. One offered-and-ignored treasure prompt and every idle remark that player ever made would read as tending their adventurer, forever. - the command test ran 50 IsCommand passes over the body on every message the bot sees, in every room. One tokenise-and-look-up does the same job.
This commit is contained in:
@@ -189,3 +189,73 @@ func TestLastExpeditionByZoneScans(t *testing.T) {
|
||||
"the picker relies on this to rotate zones", got[ZoneGoblinWarrens], got[ZoneCryptValdris])
|
||||
}
|
||||
}
|
||||
|
||||
// The deploy this column ships in: last_player_action_at is NULL for everyone
|
||||
// alive, and created_at is when the character was *made* — months ago for the
|
||||
// regulars. Without a seed, the very first tick after restart reads the whole
|
||||
// server as idle-since-creation and marches all of them into a dungeon, coins
|
||||
// debited, including the player who typed a command a minute before the deploy.
|
||||
func TestBootstrapSeedsClockSoADeployDoesNotEmptyTheTown(t *testing.T) {
|
||||
newBoredomTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
longAgo := now.Add(-90 * 24 * time.Hour)
|
||||
recent := now.Add(-30 * time.Minute)
|
||||
|
||||
// A regular: made months ago, played half an hour before the deploy.
|
||||
seedBoredomPlayerActive(t, "@regular:test", longAgo, &recent, nil)
|
||||
// Genuinely abandoned: made months ago, last touched the game months ago.
|
||||
seedBoredomPlayerActive(t, "@lapsed:test", longAgo, &longAgo, nil)
|
||||
|
||||
bootstrapBoredomClock()
|
||||
|
||||
uids, err := loadBoredomCandidates(now)
|
||||
if err != nil {
|
||||
t.Fatalf("loadBoredomCandidates: %v", err)
|
||||
}
|
||||
got := map[id.UserID]bool{}
|
||||
for _, u := range uids {
|
||||
got[u] = true
|
||||
}
|
||||
if got["@regular:test"] {
|
||||
t.Error("a player who acted 30 minutes ago was sent into a dungeon by the deploy")
|
||||
}
|
||||
if !got["@lapsed:test"] {
|
||||
t.Error("the abandoned character is exactly who this feature is for, and it skipped them")
|
||||
}
|
||||
if playerIsIdle("@regular:test", now) {
|
||||
t.Error("playerIsIdle still reads an active player as idle — Robbie would go silent for them")
|
||||
}
|
||||
}
|
||||
|
||||
// Same seed, but the character is already out on a bored expedition
|
||||
// (last_boredom_at set). Its own autopilot bumps last_active_at every day, so
|
||||
// re-seeding from it on each restart would hand the character a fresh idle
|
||||
// clock and boredom would never fire again.
|
||||
func TestBootstrapLeavesAlreadyBoredCharactersAlone(t *testing.T) {
|
||||
newBoredomTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
longAgo := now.Add(-90 * 24 * time.Hour)
|
||||
autopilotWrite := now.Add(-10 * time.Minute)
|
||||
wentOut := now.Add(-48 * time.Hour)
|
||||
|
||||
seedBoredomPlayerActive(t, "@bored:test", longAgo, &autopilotWrite, &wentOut)
|
||||
bootstrapBoredomClock()
|
||||
|
||||
if !playerIsIdle("@bored:test", now) {
|
||||
t.Fatal("restart re-seeded a bored character's clock off its own autopilot writes; it would never get bored again")
|
||||
}
|
||||
}
|
||||
|
||||
// seedBoredomPlayerActive seeds a row that predates the boredom column:
|
||||
// last_player_action_at NULL, with a last_active_at the autopilot/saves have
|
||||
// been keeping current.
|
||||
func seedBoredomPlayerActive(t *testing.T, uid id.UserID, created time.Time, lastActive, lastBoredom *time.Time) {
|
||||
t.Helper()
|
||||
_, err := db.Get().Exec(
|
||||
`INSERT INTO player_meta (user_id, display_name, alive, created_at, last_active_at, last_player_action_at, last_boredom_at)
|
||||
VALUES (?, ?, 1, ?, ?, NULL, ?)`,
|
||||
string(uid), "Test", created, lastActive, lastBoredom)
|
||||
if err != nil {
|
||||
t.Fatalf("seed player_meta: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user