From a1ede53fa9cfe6251d33cadde8cd6dc5c555f01e Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:12:22 -0700 Subject: [PATCH] Equalize co-op gift magnitudes to kill 'always leave' dominant strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Old values (open ±7, leave ±4) had matching EV but asymmetric variance; risk-averse parties had a strict preference to always leave gifts, which collapsed the dilemma. New values (all ±6) keep EV=0 for both strategies AND equalize variance. Choice now depends entirely on inferring sender intent. Adds TestCoopGiftVarianceSymmetric — fails loud if anyone reintroduces asymmetric magnitudes. EV test was already in place but only checked the mean; missed this on first pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 6 +++--- internal/plugin/coop_dungeon_gifts.go | 24 ++++++++++++------------ internal/plugin/coop_dungeon_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4f3fe19..94a4b11 100644 --- a/README.md +++ b/README.md @@ -626,12 +626,12 @@ Multi-day party runs separate from solo. 2–4 players, 2–7 days depending on **Spectator betting.** Parimutuel pool, 10% rake. Bets can be placed or increased any time during the run; positions are locked in (success or failure can't be switched). Party members can bet on their own run — sandbagging while heavily bet against is exploitative and visible. Odds line shows estimated success% based on party levels, pets, neutral funding assumption, and a hidden TwinBee Helpfulness Rating (rolling last 30 floor events: how often his recommendation was right). Helpfulness shifts the line ±20% but is never shown directly — players notice the line moves after events resolve. -**Gifts.** Anyone *not* in the run can spend one harvest action to send a Care Basket or a Mimic. The party never sees which type. Vote `open` or `leave`. Modifiers are EV-symmetric at a 50/50 sender mix: +**Gifts.** Anyone *not* in the run can spend one harvest action to send a Care Basket or a Mimic. The party never sees which type. Vote `open` or `leave`. Magnitudes are equalized across all four outcomes — both "always open" and "always leave" yield EV=0 with σ=6, so neither is risk-averse-dominant. The choice is purely about reading sender intent. | | Open | Leave | |-------------|------|-------| -| **Care Basket** | +7% (boost) | −4% (basket explodes) | -| **Mimic** | −7% (mimic does what mimics do) | +4% (sad mimic helps anyway) | +| **Care Basket** | +6% (boost) | −6% (basket explodes) | +| **Mimic** | −6% (mimic does what mimics do) | +6% (sad mimic helps anyway) | Multiple gifts stack additively — coordinated senders can swing odds significantly. The full gift log is public at end of run. diff --git a/internal/plugin/coop_dungeon_gifts.go b/internal/plugin/coop_dungeon_gifts.go index 9a37fe7..b695982 100644 --- a/internal/plugin/coop_dungeon_gifts.go +++ b/internal/plugin/coop_dungeon_gifts.go @@ -16,21 +16,21 @@ import ( // ── Gift Constants ────────────────────────────────────────────────────────── // -// Modifiers chosen so the EV of "always open" vs "always leave" is symmetric -// at a 50/50 sender mix — the party has to read the context, not the math. +// Magnitudes equalized across all four outcomes so neither "always open" nor +// "always leave" is variance-dominant at a 50/50 sender mix. Asymmetric +// magnitudes (e.g., open=±7, leave=±4) gave leave the same EV with lower +// variance, making it a dominant strategy for risk-averse parties — which +// collapses the dilemma into "always leave." // -// basket opened: +7 basket unopened: -4 -// mimic opened: -7 mimic unopened: +4 +// basket opened: +6 basket unopened: -6 +// mimic opened: -6 mimic unopened: +6 // -// At 50/50 sender mix: -// always open EV = 0.5×(+7) + 0.5×(−7) = 0 -// always leave EV = 0.5×(−4) + 0.5×(+4) = 0 - +// Both strategies: EV=0, σ=6. The choice becomes "read sender intent" only. const ( - coopGiftBasketOpened = 7 - coopGiftBasketUnopened = -4 - coopGiftMimicOpened = -7 - coopGiftMimicUnopened = 4 + coopGiftBasketOpened = 6 + coopGiftBasketUnopened = -6 + coopGiftMimicOpened = -6 + coopGiftMimicUnopened = 6 ) const ( diff --git a/internal/plugin/coop_dungeon_test.go b/internal/plugin/coop_dungeon_test.go index 34c664b..2a76c0f 100644 --- a/internal/plugin/coop_dungeon_test.go +++ b/internal/plugin/coop_dungeon_test.go @@ -293,6 +293,31 @@ func TestCoopGiftEVSymmetric(t *testing.T) { } } +func TestCoopGiftVarianceSymmetric(t *testing.T) { + t.Parallel() + // Equal EV is not enough — if open and leave have different variance, a + // risk-averse party always picks the lower-variance side and the + // dilemma collapses. Magnitudes must be equal across all four outcomes. + abs := func(x int) int { + if x < 0 { + return -x + } + return x + } + mags := []int{ + abs(coopGiftBasketOpened), + abs(coopGiftBasketUnopened), + abs(coopGiftMimicOpened), + abs(coopGiftMimicUnopened), + } + for i := 1; i < len(mags); i++ { + if mags[i] != mags[0] { + t.Errorf("gift magnitudes must be equal across all four outcomes; got %v", mags) + break + } + } +} + func TestCoopParimutuelNoWinners(t *testing.T) { t.Parallel() bets := []CoopBet{