Files
gogobee/internal/plugin/mischief_pricing_sweep_test.go
prosolis 8fc5a82b83 mischief: you can now pay to have someone's expedition ruined
Mischief Makers M1 — the core engine, Matrix-only. `!mischief send elite @user`
debits the buyer, tells the games room a hit is out, and an hour later a monster
from the target's own level bracket walks into whatever dungeon they're in.
Survive it and they keep a cut of the money and the buyer is named; don't, and
they wake up on a cart home.

The monster comes from the target's bracket zone pool, not the arena ladder and
not the dungeon they happen to be standing in — the same selection code the M0
pricing sweep ran through, so the fee table can't drift away from the fight it
priced.

Three things that are load-bearing and don't look it:

  * Survival is read off the target's HP, not PlayerWon. The engine's timeout is
    a retreat, not a lethal blow — somebody 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.

  * Nobody dies for money, and that includes the party. The delivery skips
    closeOutZoneWin/Loss (the fight is extrinsic to the dungeon — crediting it
    would let a buyer unlock the target's kill-gated resources for them), so
    nothing else floors a downed seat. Without floorMischiefRoster on BOTH
    outcomes, a member the leader outlived is left alive at 0 HP, which every
    `HPCurrent <= 0` gate reads as broken rather than dead.

  * 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 the
    same victim would both pass an in-code test. The loser is refunded.

Payouts are a percentage of the base fee, never of what the buyer actually paid —
the sign surcharge is a pure sink. Capped at 75%, so a survival purse is always
strictly less than the outlay and collusion loses to !baltransfer, which is free.
That cap is the entire anti-collusion story; no danger multiplier needed.

A crash between claiming a contract and closing it out used to be unrecoverable
in the design: the row would strand, the target could never be targeted again,
and the buyer's money was gone. The stale sweep refunds those in full — that one
is our fault, not a bet they lost.

Contract timestamps bind as Go time.Time, never CURRENT_TIMESTAMP. The driver
stores RFC3339 and SQLite's own stamp is space-separated; the two compare
lexicographically wrong.

Pete learns four mischief_* event types in a separate commit, and has to deploy
BEFORE this does — an unknown event_type is a 400, which retries and then parks
the bulletin forever.
2026-07-13 20:03:20 -07:00

86 lines
2.8 KiB
Go

package plugin
// M0 pricing sweep for gogobee_mischief_plan.md — single-fight survival per
// mischief tier, measured against the class-balance harness builds. Skip-gated:
// it is a pricing instrument, not a regression test.
//
// It drives the SAME production selection code a live delivery does
// (mischiefBracketZone + mischiefMonsters), so the fee table can't silently
// drift away from the fight it was priced against. What it does NOT share is the
// delivery path itself: this measures a full-HP build in a single chain, which
// is optimistic — a real target is wounded mid-run, and may have a party. The
// plan's M1 close-out item is to re-run this through runMischiefInterrupt.
//
// Run: MISCHIEF_SWEEP=1 go test ./internal/plugin -run TestMischiefPricingSweep -v
import (
"fmt"
"math/rand/v2"
"os"
"testing"
)
// one trial: full-HP build fights the tier's chain; survival = won every fight.
func mischiefTrial(p classBalanceProfile, tierKey string, rng *rand.Rand) (survived bool, endHPPct float64) {
c := buildHarnessCharacter(p)
zone := mischiefBracketZone(p.Level)
chain, ambush := mischiefMonsters(tierKey, zone, rng)
hp := c.HPMax
for i, m := range chain {
if ambush && i == 0 {
hp -= clampSurpriseNick(surpriseRoundNick(m, int(zone.Tier)), hp, c.HPMax)
}
player := buildHarnessPlayer(c)
if hp < c.HPMax {
player.Stats.StartHP = hp
}
enemy := buildHarnessZoneEnemy(m, int(zone.Tier))
if spell, slot, ok := pickBestDamageSpell(c); ok {
applyHarnessSpellCast(c, spell, slot, &player.Stats, &player.Mods, &enemy.Stats)
}
res := SimulateCombat(player, enemy, dungeonCombatPhases)
if !res.PlayerWon {
return false, 0
}
hp = res.PlayerEndHP
}
return true, float64(hp) / float64(c.HPMax)
}
func TestMischiefPricingSweep(t *testing.T) {
if os.Getenv("MISCHIEF_SWEEP") == "" {
t.Skip("set MISCHIEF_SWEEP=1 to run")
}
const trials = 2000
rng := rand.New(rand.NewPCG(20260713, 0x6D69736368696566))
profiles := []classBalanceProfile{
{Class: ClassPaladin, Level: 1},
{Class: ClassMage, Level: 4},
{Class: ClassMage, Level: 8, Subclass: SubclassEvocation},
{Class: ClassFighter, Level: 12, Subclass: SubclassChampion},
{Class: ClassRogue, Level: 20, Subclass: SubclassArcaneTrickster},
{Class: ClassCleric, Level: 20, Subclass: SubclassLifeDomain},
}
fmt.Printf("%-28s %-8s %8s %10s\n", "profile", "tier", "survive%", "avgEndHP%")
for _, p := range profiles {
for _, tier := range mischiefTiers {
wins := 0
hpSum := 0.0
for i := 0; i < trials; i++ {
ok, hpPct := mischiefTrial(p, tier.Key, rng)
if ok {
wins++
hpSum += hpPct
}
}
avgHP := 0.0
if wins > 0 {
avgHP = hpSum / float64(wins) * 100
}
fmt.Printf("%-28s %-8s %7.1f%% %9.1f%%\n",
fmt.Sprintf("%s L%d %s", p.Class, p.Level, p.Subclass), tier.Key,
float64(wins)/trials*100, avgHP)
}
}
}