feat/team-roles-from-registration-sheet #2

Open
renkar wants to merge 64 commits from renkar/tipibot:feat/team-roles-from-registration-sheet into master
First-time contributor

T

T
renkar added 64 commits 2026-08-28 12:06:15 +00:00
Reviewed-on: renkar/tipibot#1
Reviewed-on: renkar/tipibot#2
Multi-agent audit of the money-moving economy modules surfaced three
confirmed correctness bugs. All three are fixed here with regression tests.

heist (core/economy/heist.py):
- `house = await get_user(house.HOUSE_ID)` shadowed the imported `house`
  module for the whole function, so `house.HOUSE_ID` raised UnboundLocalError
  on every successful heist (win payout was entirely dead) and on the
  fail-path compensation branch. Rename the local to `house_rec`.
- Un-shadowing exposed a latent mint: the pot was floored at 300 but the
  house was debited only min(total, balance), so a poor house paid out more
  than it lost. Cap the pot at the balance and debit exactly what is paid
  (house debit == sum of payouts). No mint, no leak.
- Add the missing `_is_jailed` import (do_heist_check referenced it unimported).

blackjack (commands/economy_games_commands.py):
- Button callbacks had no reentrancy guard; discord.py dispatches each click
  as its own task, so double-clicking Stand within the dealer-reveal window
  paid out twice (mint), and double-clicking Double/Split deducted the extra
  bet twice. Add a synchronous `_busy` guard (matching the existing RpsGame
  idiom) on all four callbacks plus a `_resolved` idempotency flag on
  settlement, so a game can only pay out once.

bail (core/economy/jail.py, commands/economy_extra_commands.py):
- do_bail only checked balance, never jail state; a double-click or a stale
  BailView from a re-run /jailbreak charged bail twice, destroying coins (bail
  is a pure sink). Make do_bail a no-op when the user is not jailed, and add a
  UI reentrancy guard + "already free" message.

Tests: 47 passed (4 new regression tests covering heist coin-conservation and
bail idempotency).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewed-on: renkar/tipibot#3
A follow-up reentrancy sweep of the five remaining money-moving views found
one more instance of the same bug class. The other four (quests, prestige,
fish, shop) verified clean - their underlying do_* functions are idempotent.

RequestView / FundModal (commands/economy_support_commands.py):
- on_submit read self._view.remaining, then awaited do_give, then decremented
  remaining. Because discord.py dispatches each modal submit as its own task
  and do_give is a plain non-idempotent transfer, a funder could open two
  modals and submit both before the first resolved: both read the same
  pre-decrement remaining, both passed the range check, and both transferred
  `amount` - over-funding the request (remaining goes negative) and moving up
  to the funder's whole balance.
- Fix: reserve the amount synchronously (decrement remaining BEFORE the do_give
  await, with no await in between - atomic under asyncio), and roll the
  reservation back if the transfer fails. The second concurrent submit now
  sees the reduced remaining and is rejected. Added a cheap _fund guard
  (remaining<=0 / is_finished) so a click on a funded request doesn't open a
  dead modal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
core/economy has long been a package (store/income/gambling/shop/levels/
jail/heist/prestige/fishing/quests/leaderboards/house/admin) rather than a
single core/economy.py, but README.md and docs/DEV_NOTES.md still described
it as one file in ~27 places. Point every reference at the real submodule,
add a "Defined in" column to the constants quick-reference and a Module
column to the strings table, and fix the stale FISH -> FISH_CATALOGUE name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
strings/ is split into domain submodules whose names are re-exported from
strings/__init__.py. Adding a constant to a submodule and forgetting to
re-export it - or shadowing a name across two submodules - would only
surface as a runtime crash in a command. This test asserts every submodule
__all__ entry is reachable as strings.NAME, that no name is defined twice,
and that strings.__all__ matches the union of the submodules. Submodules
are auto-discovered, so a new one is covered without editing the test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The economy had many coin faucets but almost no sinks: the /shop is
one-time ownership, and gambling/rob fines route to the house (which
players drain back via jackpots and heists), so they recirculate rather
than destroy coins. Result: steady inflation.

Add a consumables shop as a true recurring sink - buying destroys the
coins and grants a temporary boost, so there's always something to spend
on after gear is maxed:

  - Energiajook XL (500) - 1h of 2x earnings on /work, /beg, /crime
  - XP jook (500)        - 1h of 2x EXP
  - Kohv (300)           - instantly clears all cooldowns

Timed buffs live in a new active_buffs field ({kind: expiry_iso}), pruned
on read; rebuying extends the timer. Effects hook where they belong:
earn_mult in income.do_work/do_beg/do_crime, exp_buff_mult in
levels.award_exp; kohv is self-contained. New /consumables command browses
the menu (with active buffs) or buys a boost. Covered by 9 tests.

Note: active_buffs is a new PocketBase field - run
scripts/sync_pb_schema.py before deploying or buffs won't persist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewed-on: renkar/tipibot#4
Reviewed-on: renkar/tipibot#5
Match members by Discord username against the lineup nicknames in the
separate registration spreadsheet (TEAM_SHEET_ID) and give each their
team's role. One team per person: switching teams removes the old team
role, and a team with no Discord role yet is auto-created. Only role
names present in the sheet are ever touched, so organisation/field/base
roles are never at risk; the feature is a no-op when TEAM_SHEET_ID is
unset.

The sheet is not a single table (merged rows, stacked CS2/LoL sections
with different layouts), so it is parsed via raw-row scanning rather than
get_all_records. Citizenship markers like "(EST)" are used only as
player delimiters and discarded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The team-role feature was aimed at the wrong bot. Tournament participants
live in the economy/community guild, but team-role sync had been bolted onto
the dev bot's roster sync (sync_member), which bails out for anyone missing
from the internal member sheet - so it could never reach its actual audience.

Decouple it: keep the (roster-independent) team-sheet parsing, pull the team
wiring off the dev/member-sync path, and re-home it on the economy bot.

- core/member_sync: revert sync_member to add-only (drop team block +
  SyncResult.roles_removed); add roster-independent sync_team_role and a
  whole-guild sync_all_team_roles returning a reporting summary. Still only
  ever touches role NAMES present in the team sheet.
- commands/economy_team_commands: new admin-only /teamsync command.
- bot.py: hourly team_sync_hourly task (economy-only, no-op unless
  TEAM_SHEET_ID is set; first tick at boot covers startup load); register
  /teamsync under the economy profile; drop the dev-side startup load.
- commands/dev_member_commands: /check no longer refreshes teams or reports
  removed roles.
- strings + .env.example: TEAMSYNC_UI, CMD[teamsync], document TEAM_SHEET_ID.
- tests: retarget sync tests to sync_team_role; add sync_all_team_roles case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6JZkyszyDFuFtk25WBbcR
This pull request has changes conflicting with the target branch.
  • DEV_NOTES.md
  • bot.py
  • economy.py
  • strings.py
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u feat/team-roles-from-registration-sheet:renkar-feat/team-roles-from-registration-sheet
git checkout renkar-feat/team-roles-from-registration-sheet
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sass/tipibot#2