mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
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.
86 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|