Adventure: Pete learns the word "retreat"
gogobee has started filing a `retreat` bulletin — an expedition that ended with the player walking out alive. Pete had no case for it, and handleAdventureIngest answers an unrecognized event_type with 400. That failure is silent and total. gogobee's sender treats any non-2xx as a failure, retries eight times over roughly two hours, and then parks the row for good. A gogobee emitting `retreat` at a Pete who doesn't know the word would not log an error anyone reads — it would just quietly drop every retreat the realm ever files. So Pete has to learn it BEFORE gogobee starts saying it. Deploy order matters here, and it is Pete first. The copy leads with everyone coming home, because that is the true and the kind part: a retreat is a bad day, not a funeral. Bulletin tier — it rides the daily digest and does not interrupt the room. Announcing every failed run live would be a firehose, and an unkind one. https://claude.ai/code/session_01B2MwktU4RgfWkar8HM3zZn
This commit is contained in:
@@ -188,6 +188,8 @@ func advEventMeta(eventType string) (label, emoji string) {
|
|||||||
return "Pete's duels", "🤝"
|
return "Pete's duels", "🤝"
|
||||||
case "milestone":
|
case "milestone":
|
||||||
return "Milestone", "🏅"
|
return "Milestone", "🏅"
|
||||||
|
case "retreat":
|
||||||
|
return "Pulled out", "🎒"
|
||||||
}
|
}
|
||||||
return "Dispatch", "📣"
|
return "Dispatch", "📣"
|
||||||
}
|
}
|
||||||
@@ -375,6 +377,19 @@ func renderAdventure(f AdvFact) (headline, lede string, ok bool) {
|
|||||||
case "death":
|
case "death":
|
||||||
return fmt.Sprintf("We lost %s in %s.", f.Subject, f.Zone),
|
return fmt.Sprintf("We lost %s in %s.", f.Subject, f.Zone),
|
||||||
fmt.Sprintf("Sad news to pass along: %s fell at level %d in %s. The graveyard's a little fuller tonight. Rest easy.", f.Subject, f.Level, f.Zone), true
|
fmt.Sprintf("Sad news to pass along: %s fell at level %d in %s. The graveyard's a little fuller tonight. Rest easy.", f.Subject, f.Level, f.Zone), true
|
||||||
|
case "retreat":
|
||||||
|
// An expedition that came apart without killing anyone. Until gogobee
|
||||||
|
// started sending these, the feed had no way to say "it went badly and
|
||||||
|
// everyone lived" — so it never said it, and the classes that retreat
|
||||||
|
// often simply never appeared. Warm, not a failure notice: everyone came
|
||||||
|
// home, and that is the part Pete leads with.
|
||||||
|
howFar := "barely a day in"
|
||||||
|
if f.Count > 1 {
|
||||||
|
howFar = fmt.Sprintf("%d days in", f.Count)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s backed out of %s.", f.Subject, f.Zone),
|
||||||
|
fmt.Sprintf("%s turned around %s — %s got the better of them this time%s, and they made the call to walk out rather than push it. Everybody came home breathing, which is the bit that counts. That dungeon'll still be there next week.",
|
||||||
|
f.Subject, howFar, f.Zone, atLevel), true
|
||||||
case "arrival":
|
case "arrival":
|
||||||
return fmt.Sprintf("Welcome to the realm, %s!", f.Subject),
|
return fmt.Sprintf("Welcome to the realm, %s!", f.Subject),
|
||||||
fmt.Sprintf("A new %s just walked through the gates. Say hello if you see them out there.", f.ClassRace), true
|
fmt.Sprintf("A new %s just walked through the gates. Say hello if you see them out there.", f.ClassRace), true
|
||||||
|
|||||||
55
internal/web/adventure_retreat_test.go
Normal file
55
internal/web/adventure_retreat_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"pete/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The retreat bulletin — gogobee's newest event type.
|
||||||
|
//
|
||||||
|
// The failure this guards against is silent and total: handleAdventureIngest
|
||||||
|
// answers an unrecognized event_type with 400, and gogobee's sender treats any
|
||||||
|
// non-2xx as a failure, retries eight times over ~two hours, and then PARKS the
|
||||||
|
// row forever. So a gogobee that emits `retreat` against a Pete that doesn't
|
||||||
|
// know the word doesn't log an error anyone reads — it just quietly drops every
|
||||||
|
// retreat the realm ever files. Pete has to learn the word first, and this test
|
||||||
|
// is what says he has.
|
||||||
|
func TestAdventureIngest_AcceptsRetreat(t *testing.T) {
|
||||||
|
const token = "s3cret-token"
|
||||||
|
s, posted := newAdvServer(t, token)
|
||||||
|
|
||||||
|
f := AdvFact{
|
||||||
|
GUID: "retreat:abc:1000", EventType: "retreat", Tier: "bulletin",
|
||||||
|
Actors: []string{"Brannigan"}, Subject: "Brannigan",
|
||||||
|
Zone: "the Underforge", Level: 12, Count: 3, Outcome: "retreated",
|
||||||
|
OccurredAt: 1000,
|
||||||
|
}
|
||||||
|
if rw := postFact(t, s, token, f); rw.Code != 200 {
|
||||||
|
t.Fatalf("ingest status = %d body=%s — Pete rejected a retreat, so gogobee will "+
|
||||||
|
"retry it eight times and park it forever", rw.Code, rw.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := storage.GetStoryByGUID("retreat:abc:1000")
|
||||||
|
if err != nil || got == nil {
|
||||||
|
t.Fatalf("retreat was accepted but not stored: %v", err)
|
||||||
|
}
|
||||||
|
// It has to actually say what happened, in Pete's voice, using only the
|
||||||
|
// supplied facts.
|
||||||
|
body := got.Headline + " " + got.Lede
|
||||||
|
for _, want := range []string{"Brannigan", "the Underforge"} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Errorf("rendered retreat is missing %q: %q", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(got.Lede, "3 days in") {
|
||||||
|
t.Errorf("the day count never made it into the copy: %q", got.Lede)
|
||||||
|
}
|
||||||
|
// Bulletin, not priority: a retreat goes in the daily digest, it does not
|
||||||
|
// interrupt the room. Pete announcing every failed run live would be a
|
||||||
|
// firehose — and an unkind one.
|
||||||
|
if len(*posted) != 0 {
|
||||||
|
t.Errorf("a bulletin was posted live: %+v", *posted)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user