Adds Base.PinEvent / Base.UnpinEvent helpers (m.room.pinned_events state)
that read-modify-write the existing pin list — idempotent on both sides.
The bot needs power level for state events; failures are logged but not
fatal.
Schema: add coop_dungeon_runs.invite_post_id to remember which event to
unpin. Migration entry included.
Wired:
- !coop start: pin the invite post in the games room
- !coop cancel: unpin before posting cancellation
- coopProcessLocks (auto-lock or auto-cancel): unpin before lock/expiry post
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Combat actions are only deducted at lock, never at start or join, so
there's nothing to refund when an unjoined invite expires. Just say so.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit pass addressed concurrency races, crash-recovery double-pay risks, and
non-determinism in tie tallying.
CRITICAL — concurrency
- coopResolveFloor now acquires advUserLock for every party member (sorted by
UserID) before mutating state, so concurrent !coop fund / vote / giftvote
commands serialize against the scheduler.
CRITICAL — crash idempotency
- Add coop_dungeon_runs.last_resolved_day and coop_dungeon_members.member_payout
columns (with migration entries) for crash-resume tracking.
- Skip resolution only if last_resolved_day >= day AND status is terminal;
otherwise re-enter and finish via idempotent operations.
- On resume detection (event already has outcome), reuse the saved roll
instead of re-rolling — prevents different outcomes on retry.
- claimCoopMemberPayout / claimCoopBetPayout: atomic UPDATE...WHERE col IS NULL
pattern. Each member/bet can only be paid once; retries are silent no-ops.
- createCoopEvent now INSERT OR IGNORE — idempotent on retry.
- last_resolved_day is set as the FINAL write per floor.
HIGH — bugs
- Lazy-create floor event in resolver if missing — covers crash window
between lockCoopRun and the original createCoopEvent.
- coopTallyVote sorts tied winners alphabetically before fallback (was
non-deterministic via Go map iteration).
- Removed dead day-1 wipe combat-action refund block — refund check was
always false because midnight reset clears state before resolution.
LOW
- Log slog.Error on malformed JSON in parseCoopVotes / parseCoopFundingMap
instead of silently dropping data.
Tests
- Tally test extended to assert determinism across 50 runs (catches map-iteration
leaks).
- Added coverage for new tie-with-leader-vote-outside-winners case.
- Idempotency-guard sanity check on LastResolvedDay vs CurrentDay.
Race detector: clean. Full suite: green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>