mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
Multi-day party runs with funding decisions, TwinBee-narrated floor events, spectator parimutuel betting, basket/mimic gift system, and weighted-roll item distribution including masterwork drops at T4-T5. Schema (5 tables, 2 fewer than spec by computing helpfulness on-the-fly and skipping the loot-pending state): - coop_dungeon_runs / _members (daily funding as JSON column) - coop_dungeon_events (votes as JSON, used to derive TwinBee helpfulness) - coop_dungeon_bets / _gifts Mechanics: - 2-4 player parties, 24h invite window, locks consume one combat action - Per-floor success roll: base + sum(funding) + level/pet bonuses + event vote modifier + active gift modifiers, clamped to 5..95 - Funding tiers (none/min/std/agg/all_in) with liability cap at +8% for under-leveled players - TwinBee narrates events from existing flavor pool; 20 authored events with per-option modifiers and embedded recommendation - Parimutuel betting with 10% rake; odds line shown on lock/daily/status posts - Gift modifiers symmetric at 50/50 sender mix so no dominant strategy - Item drops on success via weight-by-contribution roll; T4 25% / T5 100% masterwork chance (random pick from existing T4/T5 defs) Balance via Monte Carlo (coop_dungeon_balance_test.go): - All tiers exceed 1.5x solo daily income at average party profile - Optimal funding strategy walks up tiers correctly (Min/Std/Std/Mixed/Agg) - All-In stays -EV at every tier (boost lever, not optimal play) Tests: parsing, liability cap, JSON roundtrips, vote tally with leader tiebreak, event meta consistency, parimutuel payouts, gift EV symmetry, weighted-roll distribution (Monte Carlo), masterwork tier gates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
204 lines
7.4 KiB
Go
204 lines
7.4 KiB
Go
package plugin
|
||
|
||
import (
|
||
"math/rand/v2"
|
||
"strings"
|
||
)
|
||
|
||
// Metadata for each TwinBee floor event. Encodes the per-option success
|
||
// modifiers and TwinBee's recommendation. Authored by reading the flavor
|
||
// entries — TwinBee's "wrong detail" hint usually points to which option is
|
||
// actually correct.
|
||
//
|
||
// Modifier ranges: −15 (clearly bad) to +12 (clearly good). Sum across
|
||
// options is roughly net-zero so randomly-voting parties don't trend +EV.
|
||
|
||
type CoopEventCategory string
|
||
|
||
const (
|
||
CoopCatObstacle CoopEventCategory = "obstacle"
|
||
CoopCatOpportunity CoopEventCategory = "opportunity"
|
||
CoopCatCrisis CoopEventCategory = "crisis"
|
||
CoopCatEncounter CoopEventCategory = "encounter"
|
||
)
|
||
|
||
type coopEventOption struct {
|
||
label string
|
||
modifier int
|
||
}
|
||
|
||
type coopEventMeta struct {
|
||
options []coopEventOption
|
||
recommended string // A/B/C — TwinBee's pick
|
||
}
|
||
|
||
// One entry per event in the corresponding flavor pool, in the same order.
|
||
// Keep these arrays in sync with coop_flavor_twinbee.go.
|
||
|
||
var coopObstacleMeta = []coopEventMeta{
|
||
// 1. Cave-in: "definitely the left side" — too emphatic; clearing properly is right.
|
||
{options: []coopEventOption{{"A", -8}, {"B", 0}, {"C", 12}}, recommended: "A"},
|
||
// 2. Locked gate: "third tumbler might be a pressure plate" — A triggers it.
|
||
{options: []coopEventOption{{"A", -12}, {"B", -3}, {"C", 10}}, recommended: "A"},
|
||
// 3. Other party with axe: "axe is just decoration" — almost certainly not.
|
||
{options: []coopEventOption{{"A", -15}, {"B", 8}, {"C", -5}}, recommended: "A"},
|
||
// 4. Flooded section: "torches stopped twenty meters back" — water deeper than claimed.
|
||
{options: []coopEventOption{{"A", -10}, {"B", 0}, {"C", 10}}, recommended: "A"},
|
||
// 5. Collapsed bridge: "chains might be part of a trap" — they are.
|
||
{options: []coopEventOption{{"A", -12}, {"B", 0}, {"C", 8}}, recommended: "A"},
|
||
}
|
||
|
||
var coopOpportunityMeta = []coopEventMeta{
|
||
// 1. Vault: "scratches around the lock from previous attempts" — trapped.
|
||
{options: []coopEventOption{{"A", -12}, {"B", 3}}, recommended: "A"},
|
||
// 2. Side chamber: "shapes seem to be breathing" — they are. Mimics or worse.
|
||
{options: []coopEventOption{{"A", -15}, {"B", 4}}, recommended: "A"},
|
||
// 3. Wounded enemy with full pack: "one eye open, just how they sleep" — ambush.
|
||
{options: []coopEventOption{{"A", -10}, {"B", 2}}, recommended: "A"},
|
||
// 4. Unmarked door: "good feeling, ignore the smell" — TwinBee's right this time.
|
||
{options: []coopEventOption{{"A", 10}, {"B", -3}}, recommended: "A"},
|
||
// 5. Merchant Thom: usually fine to browse, sometimes a trap. Slight positive.
|
||
{options: []coopEventOption{{"A", 6}, {"B", 0}}, recommended: "A"},
|
||
}
|
||
|
||
var coopCrisisMeta = []coopEventMeta{
|
||
// 1. Trap, "left lever, definitely not the right one" — C (find another way) safest.
|
||
{options: []coopEventOption{{"A", -10}, {"B", 5}, {"C", 8}}, recommended: "A"},
|
||
// 2. Equipment corrosion: "load-bearing thing might be nothing" — pay to address.
|
||
{options: []coopEventOption{{"A", 8}, {"B", -10}, {"C", 0}}, recommended: "A"},
|
||
// 3. Separated party member, guard-room hint: TwinBee actually right that they went left;
|
||
// Option C ("wait here") is fine too — cheaper and works.
|
||
{options: []coopEventOption{{"A", 4}, {"B", -2}, {"C", 6}}, recommended: "A"},
|
||
// 4. Something following, "consistent distance is reassuring" — predator pacing.
|
||
// Pay the deterrent.
|
||
{options: []coopEventOption{{"A", -8}, {"B", 8}, {"C", 0}}, recommended: "A"},
|
||
// 5. Cave-in with "active" ceiling: pay to shore up.
|
||
{options: []coopEventOption{{"A", -10}, {"B", 10}, {"C", -2}}, recommended: "A"},
|
||
}
|
||
|
||
var coopEncounterMeta = []coopEventMeta{
|
||
// 1. Guardian, "twelve seconds two out of three times" — timing window unreliable.
|
||
{options: []coopEventOption{{"A", 4}, {"B", 6}, {"C", -10}}, recommended: "C"},
|
||
// 2. Caged person, "alarm is decorative" — usually not. Trap, but freeing them is right ~half the time.
|
||
{options: []coopEventOption{{"A", 2}, {"B", -3}, {"C", 5}}, recommended: "A"},
|
||
// 3. Sleeping guard, second guard reflected in shiny — there's a second guard.
|
||
{options: []coopEventOption{{"A", -10}, {"B", 0}, {"C", 6}}, recommended: "A"},
|
||
// 4. Recurring merchant Thom — keep moving avoids whatever shenanigans.
|
||
{options: []coopEventOption{{"A", 2}, {"B", -5}, {"C", 4}}, recommended: "A"},
|
||
// 5. Unidentifiable thing — backing away is the safe call.
|
||
{options: []coopEventOption{{"A", -8}, {"B", -2}, {"C", 8}}, recommended: "C"},
|
||
}
|
||
|
||
func coopEventCategoryMeta(cat CoopEventCategory) []coopEventMeta {
|
||
switch cat {
|
||
case CoopCatObstacle:
|
||
return coopObstacleMeta
|
||
case CoopCatOpportunity:
|
||
return coopOpportunityMeta
|
||
case CoopCatCrisis:
|
||
return coopCrisisMeta
|
||
case CoopCatEncounter:
|
||
return coopEncounterMeta
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func coopEventCategoryFlavor(cat CoopEventCategory) []string {
|
||
switch cat {
|
||
case CoopCatObstacle:
|
||
return TwinBeeObstacle
|
||
case CoopCatOpportunity:
|
||
return TwinBeeOpportunity
|
||
case CoopCatCrisis:
|
||
return TwinBeeCrisis
|
||
case CoopCatEncounter:
|
||
return TwinBeeEncounter
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// pickCoopEventCategory weights categories by tier. Lower tiers favor obstacle
|
||
// and opportunity (lighter); higher tiers weight toward crisis and encounter.
|
||
func pickCoopEventCategory(tier int) CoopEventCategory {
|
||
weights := map[int]map[CoopEventCategory]int{
|
||
1: {CoopCatObstacle: 5, CoopCatOpportunity: 4, CoopCatCrisis: 1, CoopCatEncounter: 1},
|
||
2: {CoopCatObstacle: 4, CoopCatOpportunity: 3, CoopCatCrisis: 2, CoopCatEncounter: 2},
|
||
3: {CoopCatObstacle: 3, CoopCatOpportunity: 3, CoopCatCrisis: 3, CoopCatEncounter: 2},
|
||
4: {CoopCatObstacle: 2, CoopCatOpportunity: 2, CoopCatCrisis: 4, CoopCatEncounter: 3},
|
||
5: {CoopCatObstacle: 2, CoopCatOpportunity: 2, CoopCatCrisis: 4, CoopCatEncounter: 4},
|
||
}
|
||
w, ok := weights[tier]
|
||
if !ok {
|
||
w = weights[3]
|
||
}
|
||
total := 0
|
||
for _, x := range w {
|
||
total += x
|
||
}
|
||
r := rand.IntN(total)
|
||
for cat, x := range w {
|
||
if r < x {
|
||
return cat
|
||
}
|
||
r -= x
|
||
}
|
||
return CoopCatObstacle
|
||
}
|
||
|
||
// pickCoopEvent returns a random event index for the category.
|
||
func pickCoopEvent(cat CoopEventCategory) int {
|
||
pool := coopEventCategoryMeta(cat)
|
||
if len(pool) == 0 {
|
||
return 0
|
||
}
|
||
return rand.IntN(len(pool))
|
||
}
|
||
|
||
// optionLabels returns the label list for an event ("A","B" or "A","B","C").
|
||
func coopEventOptionLabels(cat CoopEventCategory, idx int) []string {
|
||
pool := coopEventCategoryMeta(cat)
|
||
if idx >= len(pool) {
|
||
return []string{"A", "B"}
|
||
}
|
||
out := make([]string, 0, len(pool[idx].options))
|
||
for _, opt := range pool[idx].options {
|
||
out = append(out, opt.label)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// optionModifier returns the success modifier for the chosen option, or 0 if
|
||
// the option is unknown.
|
||
func coopEventOptionModifier(cat CoopEventCategory, idx int, label string) int {
|
||
pool := coopEventCategoryMeta(cat)
|
||
if idx >= len(pool) {
|
||
return 0
|
||
}
|
||
for _, opt := range pool[idx].options {
|
||
if opt.label == label {
|
||
return opt.modifier
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// recommendedOption returns TwinBee's pick for the event.
|
||
func coopEventRecommended(cat CoopEventCategory, idx int) string {
|
||
pool := coopEventCategoryMeta(cat)
|
||
if idx >= len(pool) {
|
||
return "A"
|
||
}
|
||
return pool[idx].recommended
|
||
}
|
||
|
||
// validateCoopVote uppercases and validates a vote against the available options.
|
||
func validateCoopVote(cat CoopEventCategory, idx int, raw string) (string, bool) {
|
||
v := strings.ToUpper(strings.TrimSpace(raw))
|
||
for _, label := range coopEventOptionLabels(cat, idx) {
|
||
if v == label {
|
||
return v, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|