From e85ebe56f77c90d3f9cb600832243d9c5fa75cef Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:03:28 -0700 Subject: [PATCH] news: Pete learns to report a contract killing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gogobee's Mischief Makers ships four new event types — a hit going out, the target walking away from it, the target not walking away, and the monster turning up to an empty dungeon. Pete 400s an unknown event_type, gogobee retries and then parks the bulletin forever, so this deploys first. The anonymity is the story, not an implementation detail. A contract is anonymous unless the buyer paid extra to sign it, so the lede reports the money and pointedly not the name Pete doesn't have. A survival unseals the buyer whether or not they signed — that exposure is the only brake on casual griefing, and it's the one beat worth leading with. Being maimed buys you nothing: an anonymous buyer stays anonymous when the contract lands. --- internal/web/adventure.go | 39 +++++++++++++++++++++++ internal/web/adventure_test.go | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/internal/web/adventure.go b/internal/web/adventure.go index 47d49cc..e66d31b 100644 --- a/internal/web/adventure.go +++ b/internal/web/adventure.go @@ -194,6 +194,14 @@ func advEventMeta(eventType string) (label, emoji string) { return "Pulled out", "🎒" case "departure": return "Wandered off", "🚪" + case "mischief_contract": + return "Coin on their head", "😈" + case "mischief_survived": + return "They walked away", "🛡️" + case "mischief_downed": + return "The contract landed", "💀" + case "mischief_fizzled": + return "Nobody home", "🚪" } return "Dispatch", "📣" } @@ -401,6 +409,37 @@ func renderAdventure(f AdvFact) (headline, lede string, ok bool) { // the joke tells itself, and the player it's about may well be reading. return fmt.Sprintf("%s got bored and left without waiting.", f.Subject), fmt.Sprintf("No orders, no escort, no fuss — %s packed the cheapest kit on the shelf and set off into %s%s. Nobody told them to. Nobody talked them out of it either. We'll let you know how it goes.", f.Subject, f.Zone, atLevel), true + case "mischief_contract": + // Somebody paid to have a monster sent after an adventurer who is out in a + // dungeon right now. Anonymous unless the buyer paid extra to sign it, and + // the anonymity is the story: Pete reports the money, not the name he + // doesn't have. Opponent carries the buyer only when it's public. + if f.Opponent != "" { + return fmt.Sprintf("%s has put %s on %s's head.", f.Opponent, f.Stakes, f.Subject), + fmt.Sprintf("No secret about it — %s paid for a %s to go find %s out in whatever hole they're currently down, and signed the thing. It's out there looking right now. If %s comes back breathing, they keep a cut of that money.", + f.Opponent, strings.ToLower(f.Boss), f.Subject, f.Subject), true + } + return fmt.Sprintf("Someone's put %s on %s's head.", f.Stakes, f.Subject), + fmt.Sprintf("Word came in quiet: %s has been paid for a %s to go looking for %s, and whoever paid it isn't saying so. It's already out there. Survive it and the money's theirs — and we all find out who signed the cheque.", + f.Stakes, strings.ToLower(f.Boss), f.Subject), true + case "mischief_survived": + // The unseal. A survival is the only thing that names an anonymous buyer, + // and it is the whole brake on casual griefing — so Pete leads with it. + return fmt.Sprintf("%s walked away from it. It was %s who paid.", f.Subject, f.Opponent), + fmt.Sprintf("%s came for %s, and %s is the one still standing — %s richer for the trouble. The contract's been opened up, and the name inside it is %s. Make of that what you will, folks.", + f.Boss, f.Subject, f.Subject, f.Stakes, f.Opponent), true + case "mischief_downed": + who := "Nobody's saying who paid for it" + if f.Opponent != "" { + who = fmt.Sprintf("%s paid for it, and put their name on it", f.Opponent) + } + return fmt.Sprintf("%s didn't walk away.", f.Subject), + fmt.Sprintf("A %s found %s mid-run%s and put them on the floor. They're being carried home — alive, which is more than the contract asked for, but that expedition's finished. %s.", + f.Boss, f.Subject, atLevel, who), true + case "mischief_fizzled": + return fmt.Sprintf("The monster sent for %s arrived to an empty dungeon.", f.Subject), + fmt.Sprintf("Somebody spent good money to have %s ambushed, and %s had already gone home. It wandered the halls for a bit and left. Most of the fee's been refunded. The rest, the town's keeping.", + f.Subject, f.Subject), true case "arrival": 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 diff --git a/internal/web/adventure_test.go b/internal/web/adventure_test.go index c55bcfc..06d905e 100644 --- a/internal/web/adventure_test.go +++ b/internal/web/adventure_test.go @@ -311,3 +311,61 @@ func TestAdventureDisabled(t *testing.T) { t.Errorf("disabled: status = %d, want 404", rw.Code) } } + +// TestRenderMischief: gogobee's four mischief event types must all render. An +// unknown event_type is a 400 at ingest, which gogobee retries and then parks +// forever — so "Pete deploys first" only helps if Pete actually knows the types. +// +// It also pins the anonymity contract, which is the feature's whole social +// engine: an unsigned contract must not name the buyer, and a survival must. +func TestRenderMischief(t *testing.T) { + anon := AdvFact{EventType: "mischief_contract", Tier: "priority", + Subject: "Josie", Boss: "Elite", Stakes: "€350", Level: 14} + hl, lede, ok := renderAdventure(anon) + if !ok { + t.Fatal("mischief_contract did not render — ingest would 400") + } + if strings.Contains(hl+lede, "Brannigan") { + t.Error("anonymous contract leaked a buyer name") + } + if !strings.Contains(hl, "€350") { + t.Errorf("contract headline lost the stakes: %q", hl) + } + + signed := anon + signed.Opponent = "Brannigan" + hl, _, ok = renderAdventure(signed) + if !ok || !strings.Contains(hl, "Brannigan") { + t.Errorf("signed contract should name the buyer: %q (ok=%v)", hl, ok) + } + + // The unseal: a survival names the buyer whether or not they signed. + survived := AdvFact{EventType: "mischief_survived", Tier: "priority", + Subject: "Josie", Opponent: "Brannigan", Boss: "Bone Colossus", Stakes: "€228"} + hl, _, ok = renderAdventure(survived) + if !ok || !strings.Contains(hl, "Brannigan") { + t.Errorf("survival must unseal the buyer: %q (ok=%v)", hl, ok) + } + + // A downed target with an anonymous buyer stays anonymous — being maimed + // doesn't buy you the name. + downed := AdvFact{EventType: "mischief_downed", Tier: "priority", + Subject: "Josie", Boss: "Bone Colossus", Level: 14} + hl, lede, ok = renderAdventure(downed) + if !ok { + t.Fatal("mischief_downed did not render") + } + if strings.Contains(hl+lede, "Brannigan") { + t.Error("anonymous buyer named on a downed contract") + } + + if _, _, ok := renderAdventure(AdvFact{EventType: "mischief_fizzled", Subject: "Josie", Stakes: "€315"}); !ok { + t.Error("mischief_fizzled did not render") + } + + for _, et := range []string{"mischief_contract", "mischief_survived", "mischief_downed", "mischief_fizzled"} { + if lbl, _ := advEventMeta(et); lbl == "Dispatch" { + t.Errorf("%s has no permalink label", et) + } + } +}