Files
gogobee/gogobee_mischief_plan.md
prosolis 6d402343e6 games: gogobee learns to poll Pete for money it has to move
Pete holds the chips; we hold the euros and are the only one who can move
them. This is the loop that turns an escrow row on games.parodia.dev into a
real balance change here.

It is a poll because Pete cannot call us and isn't going to be able to. Every
step is keyed on the escrow guid, because every step can be interrupted in the
worst possible place: die after DebitIdem and before the verdict is queued,
and Pete re-offers the row, we claim it again, the debit replays as a no-op
and reports the same answer. The player is charged once. That is the only
property here that really matters, and there is a test that kills us three
times to prove it.

The verdict rides the queue that already carries adventure facts — it wants
the same durability, backoff and parking, so the row now says where it's going
rather than the queue growing a twin. Flush sends it at once instead of after
a 15s sender tick, because there's a person at the other end watching a
spinner.

First GET gogobee has ever made to Pete.
2026-07-13 23:00:11 -07:00

21 KiB
Raw Blame History

Mischief Makers — paid monster hits on live expeditions

Working-tree plan doc. Do not commit.

The pitch (restated)

Parodia members spend euros to send a monster — grunt / mob / elite / boss — at a player who is out on an expedition. Orders come from Pete's Adventure web page (and from Matrix). The games room hears that a contract is out and gets a window to help the target… or "help" them. Surviving an attempt pays the target.

What the codebase already gives us (verified 2026-07-13)

Need Existing machinery
Currency + escrow EuroPlugin.Debit/Credit (euro.go:364,:395), duel escrow pattern (adventure_duel.go:422,:529,:667), community pot (adventure_rival.go:193-241)
"Is X out right now?" getActiveExpedition (dnd_expedition.go:215), roster already pushed to Pete every 2 min
Mid-run combat injection runHarvestInterrupt (dnd_expedition_combat.go:121) — picks enemy, surprise nick, runZoneCombatRoster, full win/retreat/death close-out. This is the template.
Safe-delivery gating ambient ticker's CAS claim + hasActiveCombatSession + quiet-window checks (expedition_ambient.go:97,:109,:156)
Grunt→boss ladder Arena tiers 15 (adventure_arena_monsters.go), arenaMonsterToTemplate (:251); zone pools tag IsElite
Games-room announce gamesRoom() + world-boss announce helpers (adventure_worldboss.go:528-566)
Respond-within-window duel challenge window + atomic single-winner claim (adventure_duel.go:43,:85)
Survival payout kit euro.Credit, rollAdvTreasureDrop, consumableCache, renown, world-boss payout bundle (adventure_worldboss.go:466-493)
Pete pipe durable fact queue, bearer auth, event-type taxonomy; strictly one-way gogobee→Pete today

Hard constraint (Pete internal/web/roster.go:23-25): "Pete has no route back into the game box's network and we are not opening one." We honor that: the web storefront stores orders in Pete's DB and gogobee polls Pete (gogobee stays the only initiator, same tailnet + bearer pattern as ingest, just a GET).

Design

Contract lifecycle

placed ──announce──▶ open (help/"help" window, default 60 min)
   │                    │
   │                    ├──▶ delivered  (next safe tick: target active, no combat,
   │                    │                not in briefing/recap quiet window)
   │                    │       ├── target survives  → SURVIVED payout + unseal buyer
   │                    │       └── target defeated  → DOWNED consequences
   │                    └──▶ fizzled    (expedition ended first → refund minus rake)
   └── rejected at claim time (funds/eligibility) → never announced
  • New table mischief_contracts: id, buyer_id, target_id, tier, fee, status (pending|open|delivered|fizzled|rejected), signed (bool), escalation_count, blessing json, source (matrix|web), created_at, window_ends_at, resolved_at. Status transitions via conditional UPDATE (CAS), mirroring deliverAmbient.
  • One live contract per target at a time; delivery fires from a sibling of the ambient ticker after window_ends_at.

The four tiers — PRICED (M0 done 2026-07-13)

Monster strength is relative to the target: pull from the target's own bracket zone pools (grunt/mob = non-elite, elite = IsElite, boss = the zone boss). NOT the arena ladder — arena BaseLethality is a legacy death chance; arena T5 one-shots L20s and would make every tier an execution.

M0 survival sweep (n=2000/cell, SimulateCombat via the class-balance harness builds at full HP, zone-pool monsters, ambush nick on elite/boss; throwaway harness left skip-gated at internal/plugin/mischief_pricing_sweep_test.go, env MISCHIEF_SWEEP=1):

target build grunt mob (3 chained) elite boss
paladin L1 99.8% 98.2% 57.6% 15.3%
mage L4 100% 97.8% 49.5% 0.0%
mage L8 evoc 100% 100% 56.5% 0.4%
fighter L12 champ 100% 100% 100% 99.9%
rogue L20 AT (prosolis) 100% 100% 100% 35.9%
cleric L20 life (holymachina) 100% 100% 100% 14.1%

Reading: grunt/mob are pure theater (+HP/supply attrition); elite is a real coin-flip for at-bracket targets and trivial for overleveled martials; boss is expedition-ending for casters at any level and a genuine 1-in-3 scare even for the L20 rogue. Caveats: harness builds ≠ real sheets, full-HP-at-delivery is optimistic (mid-run wounds raise real danger), no pets/party/consumables.

Prod economy snapshot (2026-07-13, 23 wallets): median balance ≈ €500; whales prosolis €180k / holymachina €149k / nonk €62k hold 89% of all money. Daily earn: whales ~€1.82.3k, mid-tier €20500, casuals <€10. Reference sinks: lottery ticket €100, duel escrow €1.55k, daily share ~€455, endgame shop items €1145k. Only 5 characters exist as targets (two L20s, L4, two L1s) — the whales are both the richest buyers and the only high-bracket targets, so boss-tier is effectively their PvP toy; casuals buy theater.

Tier Fee Signed (+25%) Survival payout (of fee) Extras on survival
grunt €40 €50 40% → €16 flavor line
mob €100 €125 50% → €50 treasure roll (standard)
elite €350 €438 65% → €228 treasure roll (elite) + renown
boss €1,200 €1,500 75% → €900 elite treasure + consumable cache + renown + Pete milestone

Rationale: grunt under the lottery-ticket impulse line so anyone can play; mob = exactly one lottery ticket; elite ≈ one active day of mid-tier earnings (a considered purchase, and its ~50% at-bracket survival odds are honest stakes); boss in duel-stake territory — whale money, priced like the "end an expedition" button it is against caster targets. Escalation cost = the tier-delta fee; blessings €25 flat.

Anti-collusion invariant: total payout (fee + escalations) capped at 75% in every case, so collusion is strictly dominated by !baltransfer, which is free. No danger multiplier needed — the cap does all the work.

Money flow: survival payout to target, remainder → community pot. Defeat → entire fee to community pot (never back to the buyer — glory only). Fizzle → 90% refund, 10% rake to pot.

Boss-tier friction (014% caster survival means "boss ≈ certain maiming"): max 1 boss contract per target per week, on top of the global limits below.

Death policy (recommendation: mischief maims, it doesn't murder)

runHarvestInterrupt's loss path perma-kills (abandonZoneRun + forcedExtractExpedition + mark dead). For a purchased attack that lands while the victim is offline, perma-death feels like paid murder, not mischief. Recommended: on defeat, apply the world-boss HP floor (worldBossFloorHP pattern), force-extract the expedition (run-loss seam already exists), drop a chunk of un-banked expedition coins/supplies, +threat. Character lives. Open decision — could make boss tier lethal for stakes, but then it needs an opt-in (hardcore flag) or it's a griefing lever.

Anonymity + the unseal twist

Default: contracts are anonymous — "someone in town has put coin on 's head." Buyer can pay +25% to sign it (taunt rights). If the target survives, the contract is unsealed — Pete names the buyer in the survival bulletin. Risk of exposure is the natural brake on casual griefing, and it makes survival announcements delicious.

The games-room window (help or "help")

On placement, announce in GAMES_ROOM (world-boss style) + Pete priority beat. During the window (default 60 min):

  • !mischief bless @target — pay a small fee (~€15) for temp HP / +AC on the incoming fight (well-rested-style bundle). Stacks, capped (say 3 blessings).
  • !mischief escalate @target — pay ~half the next-tier delta to bump the contract one tier, max one step total, boss can't escalate. Escalation money joins the payout escrow — piling on raises the target's survival jackpot.
  • Victim gets a TwinBee DM (first person, per voice rules): word has reached them that someone wants them dead. Pure flavor — expeditions are autonomous — but it lets them rally blessings.

Blessing/escalation state lives on the contract row (real rows, no phantom per-fight state — lesson from the companion free-lunch bugs).

Delivery mechanics

New runMischiefInterrupt, modeled line-for-line on runHarvestInterrupt: pick monster per tier table → apply blessings to the roster → surprise nick (attacker's privilege) → runZoneCombatRoster(fightRoster(target), …) → custom close-out. Do not call recordZoneKill/advance zone state — the fight is extrinsic to the dungeon. Party seats fight together and earn seat XP as usual; the survival purse goes to the target (leader).

Fizzle: if the expedition ends before delivery, refund fee minus 10% rake (deadpan Pete line: the monster arrived to an empty dungeon).

Rate limits & eligibility (anti-grief)

  • Target must have an active expedition and be level ≥ 3 (or similar floor).
  • One live contract per target; per-target cooldown 24 h after resolution.
  • Per-buyer cap: 2 contracts/day. Can't target yourself. Escrow debited at placement via euro.Debit (respects the debt floor for free).
  • Boredom-ticker auto-expeditions: targetable (they're real runs and it's funny), but revisit if it feels bad in practice.
  • Opt-out: none for v1 — being in the world means being in the world — but keep the decision explicitly revisitable. News anonymization (!news optout) still applies to Pete-facing names as it does today.

Pete web storefront (the reverse pipe)

Decision (2026-07-13): mischief UI requires Authentik sign-in on Pete. Pete's OIDC layer already exists ([web.auth], disabled by default) and the callback already parses preferred_username from the ID token (auth.go:236-250) — it just isn't persisted into the pete_session cookie. VERIFIED 2026-07-13 on parodia.dev (matrix-mas-1, /home/reala/matrix/compose/mas/config.yaml): MAS imports localpart with action: require, template: '{{ user.preferred_username }}' — so Authentik username == Matrix localpart, guaranteed. Identity is free: add PreferredUsername to SessionUser, gogobee maps it to @<username>:<server> (lowercase it — Matrix localparts must be lowercase, Authentik usernames may not be). No link codes.

The one-way network rule (roster.go:23-25) stays intact — both new data flows are gogobee-initiated:

  • Balances out (push): gogobee already pushes a roster snapshot every 2 min; add euro-balance entries for linked users on the same tick. Pete renders "~€X" and greys out unaffordable tiers. Advisory only; up to 2 min stale is fine.
  • Orders in (poll): signed-in buyer POSTs the order form → mischief_orders row in pete.db keyed by preferred_username, status pending. gogobee's peteclient grows a poll loop: GET /api/mischief/pending (bearer) every 30 s, claims each order (POST /api/mischief/claim, idempotent on order GUID), then validates in-game.
  • Money truth lives only in gogobee: the authoritative check is euro.Debit at claim time. A stale web balance just means an occasional "order bounced — insufficient funds" status (rendered from a fact) + TwinBee DM to the buyer. Pete never writes a balance, so no double-spend surface.

Order targets: roster board (already live on /adventure) grows a "send trouble" button per entry (roster token identifies the target — already stable + non-deanonymizing). Pete renders order status from resulting facts, never from live game state.

Ops note: enabling [web.auth] on prod Pete needs the Authentik OAuth client provisioned + config.toml edit on the box (config is box-only).

New Pete event types (deploy Pete FIRST — unknown types 400 → park forever)

mischief_contract (priority: a hit is out, tier, anonymous-or-signed), mischief_survived (priority: target thwarted it — unseal buyer here), mischief_downed (priority), mischief_fizzled (bulletin), mischief_link (internal, no render — or handle out-of-band). Deadpan voice throughout; check adventure_flavor_*.go for reusable lines before writing new flavor (standing rule).

Phases

  • M0 — price the tiers. DONE 2026-07-13 (single-fight sweep + prod economy snapshot; see tier table above). Follow-up for M1 close-out: re-run the sweep through the real delivery path once runMischiefInterrupt exists, since full-HP single fights understate mid-run danger.

  • M1 — core engine, Matrix-only. DONE 2026-07-13 (uncommitted, not deployed). adventure_mischief.go (tiers/pricing/persistence/eligibility/!mischief), adventure_mischief_deliver.go (ticker + runMischiefInterrupt + close-outs), mischief_contracts table, Pete's 4 mischief_* event types. 17 tests, 4 of them end-to-end through the real fight + euro + extraction.

    Decisions taken during implementation (all inside the plan's intent):

    • Bracket monster-selection promoted out of the M0 test into prod (mischiefBracketZone/mischiefMonsters), so the sweep and the live delivery now drive the same selection code — the fee table can't drift away from the fight it priced.
    • Survival is read off the target's HP, not PlayerWon. The engine's timeout is a retreat, not a lethal blow: a target who ran out the clock with HP left held the thing off, and a bought monster that merely outlasted them hasn't earned a maiming. (M0 counted !PlayerWon as death, so real survival rates are a touch better than priced. Pricing stands.)
    • No-perma-death extended to the whole party (floorMischiefRoster, run on BOTH outcomes). A leader can win a fight their friend went down in, and the delivery skips closeOutZoneWin — without the floor, the member is left alive at 0 HP, which every HPCurrent <= 0 gate reads as broken.
    • One-live-contract-per-target is a partial UNIQUE INDEX, not a read-then- write check: placement holds only the buyer's lock, so two buyers racing at one victim would both pass an in-code check. The loser is refunded.
    • Stale-delivery sweep (delivering + 15 min grace → full refund). Without it a crash mid-fight strands the row: target permanently un-targetable, buyer's money gone.
    • All contract timestamps bind as Go time.Time — never CURRENT_TIMESTAMP. The driver stores RFC3339 (2026-07-14T03:48:52Z); a SQL-side stamp writes 2026-07-14 03:48:52, and the two compare lexicographically wrong.
    • Fizzle DMs + rake, 90% refund; unstageable/stranded contracts refund 100% (our fault, not a bet they lost).

    M1 close-out sweep DONE 2026-07-13 (mischief_delivery_sweep_test.go, MISCHIEF_SWEEP=1, n=400/cell, 108 cells through the REAL delivery path: runMischiefInterruptrunZoneCombatRoster). Arms: entry HP 100/70/40% (100% = control, reproduces M0 through the new path) × wards 0/3 on elite+boss.

    target tier 100% HP 70% HP 40% HP 70% +3 wards
    paladin L3 elite 84.8% 73.2% 62.7% 80.5%
    paladin L3 boss 87.8% 45.8% 8.0% 88.0%
    mage L4 elite 66.0% 29.5% 4.0% 64.0%
    mage L8 evoc elite 90.2% 72.2% 15.0% 84.2%
    mage L8 evoc boss 6.5% 2.0% 0.2% 7.8%
    fighter L12 boss 88.2% 48.0% 6.8% 89.8%
    rogue L20 boss 19.2% 2.0% 0.0% 18.5%
    cleric L20 boss 42.0% 6.0% 0.0% 42.8%

    (grunt/mob: 100% for everyone except mage L4, who can lose a wounded mob.)

    Three findings:

    • The M0 table was priced on a fight we don't deliver. The control arm diverges from M0 in BOTH directions: up where an engine timeout now counts as survival (paladin boss 15→88%), and down where the turn engine loops a boss's full multiattack profile and SimulateCombat doesn't (fighter boss 99.9→88%, rogue boss 36→19%) — see project_sim_multiattack_gap. Fees kept (the tiers still do what they were priced to do); this table is now the reference.
    • Wounding is the dominant variable, and M0 was blind to it. Boss at a realistic mid-run 70% HP collapses across the board (fighter 88→48, cleric 42→6, rogue 19→2); at 40% it is near-zero for everyone. Boss really is the "end their expedition" button, as priced.
    • Wards were too cheap (see M2 below): 3 of them buy ~+40pp.
  • M2 — the window. DONE 2026-07-13. !mischief bless @user (€25, cap 3, +10% MaxHP temp HP each) · !mischief escalate @user (pays the tier delta, one step, boss is the ceiling) · victim DM on placement · escalator unsealed alongside the buyer on survival. 6 new tests (22 total).

    Decisions taken during implementation:

    • Blessing fee is a pure sink (community pot), never a purse top-up. If a ward raised the payout basis, warding a friend would become a money-routing move; buying them HP can't be.
    • Ward price scales with the contractmax(€25, 10% of fee): grunt/mob €25, elite €35, boss €120 (€360 to cover someone completely). The close-out sweep measured 3 wards at ~+40pp (fighter vs boss at 70% HP: 48→90%), so a flat €25 let the room halve a €1,200 boss contract for €75 — pocket change against the tier the whole economy rests on. Reads the contract's current basis, so an escalation raises the price of saving its target too. The €25 floor is knowingly above-market at grunt (3 wards > the €40 fee): nothing needs warding against theatre, and the floor exists to keep a ward an impulse.
    • Escalation raises fee (the payout basis) AND paid together, so the 75% cap and "purse < outlay" survive an escalation untouched — asserted.
    • Escalating into boss answers to the boss-per-target-per-week cap. Otherwise the cap is bought around for the price of an elite plus the delta.
    • Both window commands are CAS-then-refund, like placement: the ward cap and the one-step limit live in the UPDATE's WHERE clause, because a scramble is exactly when four people press the button in the same second.
    • The delivery re-reads the contract AFTER claiming it. The due-sweep's copy is stale by construction — the window keeps writing to an open row up to the claim, so a last-second escalation was paid for and then delivered the old tier's monster. The claim is the fence. (Fixed + regression test.)
    • The ward is temp HP (the well-rested mechanism), added to whatever cushion the target already carried and subtracted again after the fight — a bought ward must not eat a home long rest. It refreshes per link of the mob chain; that only affects the one tier the sweep priced as theatre.
    • Blessers are named on the spot (helping is proud); escalators are anonymous until the survival unseals them, exactly like the buyer.
    • Fizzled/unstageable contracts do not refund blessers. Their €25 was charity, and it went to the pot. Revisit if it stings in practice.
    • Fixed a pre-existing wall-clock flake in the M1 fizzle test: it drove fireMischiefDeliveries with time.Now(), which refuses to run inside the ±1 h quiet windows around the 06:00 briefing and 21:00 recap.
  • M3 — Pete storefront (1 session, cross-repo). Enable + extend OIDC (persist preferred_username; provision Authentik client), orders table + form + status rendering, balance push, gogobee poll/claim loop. First visitor-facing write path on Pete — auth-gated + rate-limited per user. (MAS localpart == Authentik preferred_username: verified 2026-07-13.)

  • M4 — polish. Notoriety/renown for buyers, survival streaks → milestones, digest lines, boredom-expedition policy review, tune fees from live data.

Decisions — ALL CONFIRMED by reala 2026-07-13

  1. No perma-death anywhere — mischief maims: HP floor + force-extract + un-banked loot/supply loss + threat bump. Boss tier included.
  2. Anonymous by default; survival unseals the buyer in Pete's bulletin; +25% to sign openly.
  3. Fizzle refund 90%, 10% rake to community pot.
  4. Targets: level ≥ 3 with an active expedition; boredom auto-runs are fair game. One live contract per target, 24 h post-resolution cooldown, 2 contracts/day per buyer, 1 boss/target/week.
  5. Fee/payout table as priced above (M0 sweep + prod economy).

Standing-rule checklist

  • No new dnd_-prefixed files/tables → adventure_mischief*.go, mischief_* tables.
  • TwinBee first-person; Pete deadpan third-person announcer.
  • Plan doc stays working-tree only.
  • Pete deploys before gogobee whenever event types change.
  • Sim sweeps: control arm + n≥750; run heavy sweeps on Parodia/millenia.
  • Any loader/bootstrap added must keep its backfill as a one-shot bootstrap.