mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
mischief: price a ward against the thing it wards off
The M1 close-out sweep (new, skip-gated: n=400/cell through the REAL delivery path, not SimulateCombat) says three wards buy roughly +40pp of survival — a level 12 fighter meeting a boss at 70% HP goes from 48% to 90%. At a flat €25 that let three friends halve a €1,200 boss contract for €75, which makes the tier the entire economy rests on feel like money thrown away. A ward now costs max(€25, 10% of the contract): €25 at grunt and mob, €35 at elite, €120 at a boss — so covering someone against a boss costs the town €360. Still a bargain against €1,200, but it has to be a real rally rather than pocket change. It reads the contract's current basis, so an escalation raises the price of saving its target too: the counterplay tracks the threat. The sweep also shows the M0 fee table was priced on a fight we don't deliver. Its control arm (full HP) diverges from M0 in both directions — up where an engine timeout now counts as survival, down where the turn engine loops a boss's full multiattack profile and SimulateCombat doesn't. And the arm M0 could not see at all, a wounded mid-run target, is the one that matters: boss tier at 70% HP collapses everywhere. Fees stand; the tiers still do what they were priced to do. The real-path table is now the reference.
This commit is contained in:
@@ -75,10 +75,16 @@ const (
|
|||||||
// Fizzle: the expedition ended before the monster could find them.
|
// Fizzle: the expedition ended before the monster could find them.
|
||||||
mischiefFizzleRefund = 0.90
|
mischiefFizzleRefund = 0.90
|
||||||
|
|
||||||
// mischiefBlessingFee — what the room pays to put a ward on the target while
|
// A ward is priced against the contract it is defending: a floor, or a slice of
|
||||||
// the contract is open. Cheap on purpose: helping has to be an impulse, or
|
// the fee, whichever is more. Warding someone has to stay an impulse at the
|
||||||
// nobody bothers and the window is just a countdown.
|
// cheap tiers (or nobody bothers and the window is just a countdown), but the
|
||||||
mischiefBlessingFee = 25
|
// M1-close-out sweep measured three wards buying roughly +40pp of survival —
|
||||||
|
// fighter L12 vs a boss at 70% HP goes 48% → 90%. At a flat fee that let the
|
||||||
|
// room halve a €1,200 boss contract for €75, which makes the tier the whole
|
||||||
|
// economy rests on feel like money thrown away. Saving someone from a boss
|
||||||
|
// should cost the town real money: €120 a ward, €360 to cover them completely.
|
||||||
|
mischiefBlessingFeeMin = 25
|
||||||
|
mischiefBlessingFeePct = 0.10
|
||||||
|
|
||||||
// mischiefBlessingCap — how many blessings one contract can carry. Three at
|
// mischiefBlessingCap — how many blessings one contract can carry. Three at
|
||||||
// +10% MaxHP each is a third of a health bar: enough to swing an elite
|
// +10% MaxHP each is a third of a health bar: enough to swing an elite
|
||||||
@@ -149,6 +155,17 @@ func mischiefNextTier(key string) (mischiefTier, bool) {
|
|||||||
return mischiefTier{}, false
|
return mischiefTier{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mischiefBlessingFee is what one ward costs against a contract of this fee. It
|
||||||
|
// reads the contract's CURRENT basis, so an escalation raises the price of
|
||||||
|
// protecting its target too — the room's counterplay tracks the threat.
|
||||||
|
func mischiefBlessingFee(contractFee int) int {
|
||||||
|
fee := int(math.Round(float64(contractFee) * mischiefBlessingFeePct))
|
||||||
|
if fee < mischiefBlessingFeeMin {
|
||||||
|
return mischiefBlessingFeeMin
|
||||||
|
}
|
||||||
|
return fee
|
||||||
|
}
|
||||||
|
|
||||||
// mischiefBlessingTempHP is the cushion a contract's blessings are worth to a
|
// mischiefBlessingTempHP is the cushion a contract's blessings are worth to a
|
||||||
// target of this MaxHP. At least 1 per blessing, so a low-HP character still
|
// target of this MaxHP. At least 1 per blessing, so a low-HP character still
|
||||||
// feels the ward they were bought.
|
// feels the ward they were bought.
|
||||||
@@ -580,17 +597,18 @@ func (p *AdventurePlugin) mischiefBoard() string {
|
|||||||
b.WriteString("The monster comes from their own bracket, so it's always a fight worth watching. ")
|
b.WriteString("The monster comes from their own bracket, so it's always a fight worth watching. ")
|
||||||
b.WriteString("If they survive it, they keep a cut of your money — and the town finds out it was you.\n\n")
|
b.WriteString("If they survive it, they keep a cut of your money — and the town finds out it was you.\n\n")
|
||||||
for _, t := range mischiefTiers {
|
for _, t := range mischiefTiers {
|
||||||
b.WriteString(fmt.Sprintf("· **%s** — %s (signed: %s) — survivor keeps %s\n _%s_\n",
|
b.WriteString(fmt.Sprintf("· **%s** — %s (signed: %s) — survivor keeps %s · ward %s\n _%s_\n",
|
||||||
t.Display, fmtEuro(t.Fee), fmtEuro(mischiefSignedFee(t.Fee)),
|
t.Display, fmtEuro(t.Fee), fmtEuro(mischiefSignedFee(t.Fee)),
|
||||||
fmtEuro(mischiefPurse(t.Fee, t.PayoutPct)), t.Blurb))
|
fmtEuro(mischiefPurse(t.Fee, t.PayoutPct)), fmtEuro(mischiefBlessingFee(t.Fee)), t.Blurb))
|
||||||
}
|
}
|
||||||
b.WriteString("\n`!mischief send <tier> @user` — anonymous · add `signed` to put your name on it up front.\n")
|
b.WriteString("\n`!mischief send <tier> @user` — anonymous · add `signed` to put your name on it up front.\n")
|
||||||
b.WriteString(fmt.Sprintf(
|
b.WriteString(fmt.Sprintf(
|
||||||
"While a contract is open, the rest of us get a say:\n"+
|
"While a contract is open, the rest of us get a say:\n"+
|
||||||
"· `!mischief bless @user` — %s, up to %d — a ward for the fight. Help them.\n"+
|
"· `!mischief bless @user` — a ward for the fight, up to %d of them. Help them. "+
|
||||||
|
"The worse the thing coming, the more a ward costs.\n"+
|
||||||
"· `!mischief escalate @user` — pay the difference, send something worse. \"Help\" them. "+
|
"· `!mischief escalate @user` — pay the difference, send something worse. \"Help\" them. "+
|
||||||
"It raises their purse too, if they live.\n",
|
"It raises their purse too, if they live.\n",
|
||||||
fmtEuro(mischiefBlessingFee), mischiefBlessingCap))
|
mischiefBlessingCap))
|
||||||
b.WriteString(fmt.Sprintf("_They have to be out on an expedition, level %d+. It lands about %s later._",
|
b.WriteString(fmt.Sprintf("_They have to be out on an expedition, level %d+. It lands about %s later._",
|
||||||
mischiefMinLevel, formatDuration(mischiefWindow)))
|
mischiefMinLevel, formatDuration(mischiefWindow)))
|
||||||
return b.String()
|
return b.String()
|
||||||
@@ -764,22 +782,26 @@ func (p *AdventurePlugin) mischiefBlessCmd(ctx MessageContext, fields []string)
|
|||||||
"**%s** is already carrying every ward the town has to give (%d). The rest is up to them.",
|
"**%s** is already carrying every ward the town has to give (%d). The rest is up to them.",
|
||||||
p.DisplayName(targetID), mischiefBlessingCap))
|
p.DisplayName(targetID), mischiefBlessingCap))
|
||||||
}
|
}
|
||||||
if bal := p.euro.GetBalance(ctx.Sender); int(bal) < mischiefBlessingFee {
|
// Priced off the contract's current basis: warding someone against a boss costs
|
||||||
|
// what a boss is worth, and an escalation raises the price of saving them.
|
||||||
|
fee := mischiefBlessingFee(c.Fee)
|
||||||
|
if bal := p.euro.GetBalance(ctx.Sender); int(bal) < fee {
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf(
|
return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf(
|
||||||
"A ward costs %s — you have %s.", fmtEuro(mischiefBlessingFee), fmtEuro(int(bal))))
|
"A ward against a %s costs %s — you have %s.",
|
||||||
|
strings.ToLower(c.Tier), fmtEuro(fee), fmtEuro(int(bal))))
|
||||||
}
|
}
|
||||||
if !p.euro.Debit(ctx.Sender, float64(mischiefBlessingFee), "mischief_blessing") {
|
if !p.euro.Debit(ctx.Sender, float64(fee), "mischief_blessing") {
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Couldn't take your money. Try again.")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Couldn't take your money. Try again.")
|
||||||
}
|
}
|
||||||
// The cap is enforced by the UPDATE, not by the read above — two people can
|
// The cap is enforced by the UPDATE, not by the read above — two people can
|
||||||
// buy the third ward in the same second, and only one of them may have it.
|
// buy the third ward in the same second, and only one of them may have it.
|
||||||
if !blessMischiefContract(c.ID) {
|
if !blessMischiefContract(c.ID) {
|
||||||
p.euro.Credit(ctx.Sender, float64(mischiefBlessingFee), "mischief_blessing_refund")
|
p.euro.Credit(ctx.Sender, float64(fee), "mischief_blessing_refund")
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
"Somebody got there first — the ward wasn't needed. You've been refunded.")
|
"Somebody got there first — the ward wasn't needed. You've been refunded.")
|
||||||
}
|
}
|
||||||
communityPotAdd(mischiefBlessingFee)
|
communityPotAdd(fee)
|
||||||
trackTaxPaid(ctx.Sender, mischiefBlessingFee)
|
trackTaxPaid(ctx.Sender, fee)
|
||||||
|
|
||||||
p.announceMischiefBlessing(c, ctx.Sender, c.BlessingCount+1)
|
p.announceMischiefBlessing(c, ctx.Sender, c.BlessingCount+1)
|
||||||
p.SendDM(targetID, fmt.Sprintf(
|
p.SendDM(targetID, fmt.Sprintf(
|
||||||
@@ -902,7 +924,7 @@ func (p *AdventurePlugin) dmMischiefVictim(c *mischiefContract, tier mischiefTie
|
|||||||
"anyone who likes you can `!mischief bless @%s` (%s each, up to %d).\n"+
|
"anyone who likes you can `!mischief bless @%s` (%s each, up to %d).\n"+
|
||||||
"Walk away from it and their money is yours: **%s**. And we all find out who paid.",
|
"Walk away from it and their money is yours: **%s**. And we all find out who paid.",
|
||||||
who, strings.ToLower(tier.Display), formatDuration(time.Until(c.WindowEndsAt)),
|
who, strings.ToLower(tier.Display), formatDuration(time.Until(c.WindowEndsAt)),
|
||||||
p.DisplayName(c.TargetID), fmtEuro(mischiefBlessingFee), mischiefBlessingCap,
|
p.DisplayName(c.TargetID), fmtEuro(mischiefBlessingFee(c.Fee)), mischiefBlessingCap,
|
||||||
fmtEuro(mischiefPurse(c.Fee, tier.PayoutPct))))
|
fmtEuro(mischiefPurse(c.Fee, tier.PayoutPct))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -586,6 +586,36 @@ func mischiefCtx(sender id.UserID) MessageContext {
|
|||||||
return MessageContext{Sender: sender, RoomID: id.RoomID("!room:x"), EventID: id.EventID("$e")}
|
return MessageContext{Sender: sender, RoomID: id.RoomID("!room:x"), EventID: id.EventID("$e")}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A ward is priced against the thing it defends against. The M1-close-out sweep
|
||||||
|
// measured three wards buying ~+40pp of survival, which at a flat fee let the room
|
||||||
|
// halve a €1,200 boss contract for €75 — the tier the whole economy rests on,
|
||||||
|
// bought off for pocket change. Cheap at the impulse tiers, real money at the top.
|
||||||
|
func TestMischiefBlessingFee_ScalesWithTheContract(t *testing.T) {
|
||||||
|
want := map[string]int{"grunt": 25, "mob": 25, "elite": 35, "boss": 120}
|
||||||
|
for _, tier := range mischiefTiers {
|
||||||
|
if got := mischiefBlessingFee(tier.Fee); got != want[tier.Key] {
|
||||||
|
t.Errorf("%s ward = %d, want %d", tier.Key, got, want[tier.Key])
|
||||||
|
}
|
||||||
|
// Where a ward is a rational purchase — the tiers the sweep says can
|
||||||
|
// actually put someone on the floor — covering a target must cost less than
|
||||||
|
// the contract does. Otherwise the room's counterplay is priced out and the
|
||||||
|
// window is dead.
|
||||||
|
//
|
||||||
|
// Grunt and mob are deliberately exempt: the €25 floor is above-market
|
||||||
|
// there (3 wards cost more than a €40 grunt), and that is fine, because
|
||||||
|
// nothing in the sweep needs warding against theatre — those tiers sit at
|
||||||
|
// 100% survival unwounded. The floor is there to keep a ward an impulse
|
||||||
|
// buy, not to make it a sensible one against a goblin.
|
||||||
|
if tier.Key != "elite" && tier.Key != "boss" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if full := mischiefBlessingFee(tier.Fee) * mischiefBlessingCap; full >= tier.Fee {
|
||||||
|
t.Errorf("%s: %d wards cost %d, which is more than the %d contract — "+
|
||||||
|
"the room can't afford to save anyone", tier.Key, mischiefBlessingCap, full, tier.Fee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The cap lives in the UPDATE, not in the read that precedes it. A room that
|
// The cap lives in the UPDATE, not in the read that precedes it. A room that
|
||||||
// piles four wards on in the same second must still land exactly three — and the
|
// piles four wards on in the same second must still land exactly three — and the
|
||||||
// player who lost that race must get their money back rather than pay for
|
// player who lost that race must get their money back rather than pay for
|
||||||
@@ -629,6 +659,7 @@ func TestMischiefBlessing_FeeIsASinkNotAPurseTopUp(t *testing.T) {
|
|||||||
|
|
||||||
c := seedContract(t, "@buyer:x", target, "elite", false)
|
c := seedContract(t, "@buyer:x", target, "elite", false)
|
||||||
feeBefore := c.Fee
|
feeBefore := c.Fee
|
||||||
|
ward := mischiefBlessingFee(c.Fee) // 10% of the €350 elite fee
|
||||||
|
|
||||||
if err := p.mischiefBlessCmd(mischiefCtx(friend), []string{string(target)}); err != nil {
|
if err := p.mischiefBlessCmd(mischiefCtx(friend), []string{string(target)}); err != nil {
|
||||||
t.Fatalf("bless: %v", err)
|
t.Fatalf("bless: %v", err)
|
||||||
@@ -642,11 +673,11 @@ func TestMischiefBlessing_FeeIsASinkNotAPurseTopUp(t *testing.T) {
|
|||||||
t.Errorf("a blessing moved the payout basis to %d (was %d) — wards must not pay the target",
|
t.Errorf("a blessing moved the payout basis to %d (was %d) — wards must not pay the target",
|
||||||
got.Fee, feeBefore)
|
got.Fee, feeBefore)
|
||||||
}
|
}
|
||||||
if bal := p.euro.GetBalance(friend); bal != 500-mischiefBlessingFee {
|
if bal := p.euro.GetBalance(friend); bal != float64(500-ward) {
|
||||||
t.Errorf("blesser balance %.0f, want %d", bal, 500-mischiefBlessingFee)
|
t.Errorf("blesser balance %.0f, want %d", bal, 500-ward)
|
||||||
}
|
}
|
||||||
if pot := communityPotBalance(); pot != mischiefBlessingFee {
|
if pot := communityPotBalance(); pot != ward {
|
||||||
t.Errorf("pot holds %d, want the %d blessing fee", pot, mischiefBlessingFee)
|
t.Errorf("pot holds %d, want the %d ward fee", pot, ward)
|
||||||
}
|
}
|
||||||
// Broke, so the second ward can't be bought — and must cost nothing.
|
// Broke, so the second ward can't be bought — and must cost nothing.
|
||||||
pauper := id.UserID("@pauper:x")
|
pauper := id.UserID("@pauper:x")
|
||||||
|
|||||||
148
internal/plugin/mischief_delivery_sweep_test.go
Normal file
148
internal/plugin/mischief_delivery_sweep_test.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
// M1 close-out sweep for gogobee_mischief_plan.md — survival per tier through the
|
||||||
|
// REAL delivery path (runMischiefInterrupt → runZoneCombatRoster), not through
|
||||||
|
// SimulateCombat.
|
||||||
|
//
|
||||||
|
// The M0 sweep that priced the fee table measured a full-HP build in a single
|
||||||
|
// chain. Two things it could not see, and both of them are why this exists:
|
||||||
|
//
|
||||||
|
// - A real target is WOUNDED. The monster arrives mid-run, after the dungeon has
|
||||||
|
// already taken a bite out of them. The entryHP arm is the control-vs-treatment
|
||||||
|
// axis: 100% reproduces M0 through the new path (so a divergence there is a
|
||||||
|
// path bug, not a difficulty finding), 70% and 40% are what a live delivery
|
||||||
|
// actually lands on.
|
||||||
|
// - The M2 ward. `blessings` drives the same temp-HP cushion the room buys, so
|
||||||
|
// we can price what €75 of charity is actually worth against an elite.
|
||||||
|
//
|
||||||
|
// Run (heavy — see the plan's sim rules, n≥750 per cell for a real signal):
|
||||||
|
//
|
||||||
|
// MISCHIEF_SWEEP=1 go test ./internal/plugin -run TestMischiefDeliverySweep -v -timeout 4h
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogobee/internal/db"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mischiefDeliveryTrial seeds a real adventurer on a real expedition at the given
|
||||||
|
// fraction of their max HP, sends them the tier's monster through the production
|
||||||
|
// delivery path, and reports whether they were still standing.
|
||||||
|
//
|
||||||
|
// Survival is read the way the delivery reads it (HP > 0), not as "won every
|
||||||
|
// fight" — an engine timeout with HP left is a target who held the thing off.
|
||||||
|
func mischiefDeliveryTrial(
|
||||||
|
t *testing.T, p *AdventurePlugin, prof classBalanceProfile,
|
||||||
|
tierKey string, entryHPPct float64, blessings int, seq int, rng *rand.Rand,
|
||||||
|
) (survived bool, endHPPct float64) {
|
||||||
|
t.Helper()
|
||||||
|
uid := id.UserID(fmt.Sprintf("@sweep%d:sim", seq))
|
||||||
|
|
||||||
|
c := buildHarnessCharacter(prof)
|
||||||
|
c.UserID = uid
|
||||||
|
c.HPCurrent = int(float64(c.HPMax) * entryHPPct)
|
||||||
|
if c.HPCurrent < 1 {
|
||||||
|
c.HPCurrent = 1
|
||||||
|
}
|
||||||
|
if err := createAdvCharacter(uid, "sweep"+strconv.Itoa(seq)); err != nil {
|
||||||
|
t.Fatalf("createAdvCharacter: %v", err)
|
||||||
|
}
|
||||||
|
if err := SaveDnDCharacter(c); err != nil {
|
||||||
|
t.Fatalf("SaveDnDCharacter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
run, err := startZoneRun(uid, ZoneGoblinWarrens, prof.Level, rng)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("startZoneRun: %v", err)
|
||||||
|
}
|
||||||
|
expID := "exp-sweep-" + strconv.Itoa(seq)
|
||||||
|
if _, err := db.Get().Exec(`
|
||||||
|
INSERT INTO dnd_expedition (expedition_id, user_id, zone_id, run_id, status, start_date, coins_earned)
|
||||||
|
VALUES (?, ?, 'goblin_warrens', ?, 'active', ?, 0)`,
|
||||||
|
expID, string(uid), run.RunID, time.Now().UTC()); err != nil {
|
||||||
|
t.Fatalf("seed expedition: %v", err)
|
||||||
|
}
|
||||||
|
exp, err := getActiveExpedition(uid)
|
||||||
|
if err != nil || exp == nil {
|
||||||
|
t.Fatalf("getActiveExpedition: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bracket := mischiefBracketZone(prof.Level)
|
||||||
|
chain, ambush := mischiefMonsters(tierKey, bracket, rng)
|
||||||
|
|
||||||
|
_, _, survived = p.runMischiefInterrupt(uid, exp, bracket, chain, ambush, blessings)
|
||||||
|
hp, max := dndHPSnapshot(uid)
|
||||||
|
if max <= 0 {
|
||||||
|
max = 1
|
||||||
|
}
|
||||||
|
return survived, float64(hp) / float64(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMischiefDeliverySweep(t *testing.T) {
|
||||||
|
if os.Getenv("MISCHIEF_SWEEP") == "" {
|
||||||
|
t.Skip("set MISCHIEF_SWEEP=1 to run")
|
||||||
|
}
|
||||||
|
trials := 250
|
||||||
|
if n := os.Getenv("MISCHIEF_TRIALS"); n != "" {
|
||||||
|
if v, err := strconv.Atoi(n); err == nil && v > 0 {
|
||||||
|
trials = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newMischiefTestDB(t)
|
||||||
|
p := &AdventurePlugin{euro: NewEuroPlugin(nil)}
|
||||||
|
p.Sink = &captureSink{}
|
||||||
|
|
||||||
|
rng := rand.New(rand.NewPCG(20260713, 0x64656C6976657279))
|
||||||
|
profiles := []classBalanceProfile{
|
||||||
|
{Class: ClassPaladin, Level: 3},
|
||||||
|
{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},
|
||||||
|
}
|
||||||
|
// entryHP 1.0 is the control arm: it should reproduce the M0 table through the
|
||||||
|
// real path. The wounded arms are the finding.
|
||||||
|
entryHPs := []float64{1.0, 0.7, 0.4}
|
||||||
|
|
||||||
|
seq := 0
|
||||||
|
fmt.Printf("\n%-26s %-6s %-7s %-6s %8s %10s\n",
|
||||||
|
"profile", "tier", "entryHP", "wards", "survive%", "avgEndHP%")
|
||||||
|
for _, prof := range profiles {
|
||||||
|
for _, tier := range mischiefTiers {
|
||||||
|
for _, entry := range entryHPs {
|
||||||
|
// The ward arm only where a ward could decide anything: the tiers the
|
||||||
|
// M0 sweep did not already call theatre.
|
||||||
|
wardArms := []int{0}
|
||||||
|
if tier.Key == "elite" || tier.Key == "boss" {
|
||||||
|
wardArms = []int{0, mischiefBlessingCap}
|
||||||
|
}
|
||||||
|
for _, wards := range wardArms {
|
||||||
|
wins, hpSum := 0, 0.0
|
||||||
|
for i := 0; i < trials; i++ {
|
||||||
|
seq++
|
||||||
|
ok, hpPct := mischiefDeliveryTrial(t, p, prof, tier.Key, entry, wards, seq, rng)
|
||||||
|
if ok {
|
||||||
|
wins++
|
||||||
|
hpSum += hpPct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avgHP := 0.0
|
||||||
|
if wins > 0 {
|
||||||
|
avgHP = hpSum / float64(wins) * 100
|
||||||
|
}
|
||||||
|
fmt.Printf("%-26s %-6s %6.0f%% %6d %7.1f%% %9.1f%%\n",
|
||||||
|
fmt.Sprintf("%s L%d %s", prof.Class, prof.Level, prof.Subclass),
|
||||||
|
tier.Key, entry*100, wards, float64(wins)/float64(trials)*100, avgHP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user