Auto merge dev → main (#1339)

* feat(tally): /tally-claim, /tally-transfer, /tally-wipe commands

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tally): idle sweep, startup load, and empty-VC expiry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(tally): parenthesize voice-state guard for clarity

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tally): update live tallies when sessions finish

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tally): robust winner matching + cleanup of deleted-VC tallies

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tally): /dev-tally to manually attribute a win/loss in your VC

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
NotSoToothless
2026-06-19 01:19:19 -07:00
committed by GitHub
parent 732595433a
commit 9222f7c53f
4 changed files with 316 additions and 4 deletions
+52 -2
View File
@@ -56,13 +56,23 @@ def team_short(team: dict) -> str:
return strip_tag(team.get("squadron_short") or team.get("squadron"))
def team_identities(team: dict) -> set[str]:
"""All lowercased, tag-stripped squadron identifiers for a replay team."""
out = set()
for key in ("squadron_short", "squadron", "squadron_tagged"):
v = strip_tag(team.get(key))
if v:
out.add(v.lower())
return out
def team_index_for(tally: Tally, teams: list[dict]) -> Optional[int]:
"""Return the index of the team the tracked entity is on, else None."""
for idx, team in enumerate(teams):
if not team:
continue
if tally.mode == "squadron":
if team_short(team).lower() == strip_tag(tally.target).lower():
if strip_tag(tally.target).lower() in team_identities(team):
return idx
else: # player mode
target = tally.target.strip().lower()
@@ -96,7 +106,7 @@ def evaluate(
if is_draw:
kind = "draw"
tally.losses += 1
elif winner_short and strip_tag(winner_short).lower() == team_short(teams[idx]).lower():
elif winner_short and strip_tag(winner_short).lower() in team_identities(teams[idx]):
kind = "win"
tally.wins += 1
else:
@@ -207,6 +217,28 @@ def wipe(guild_id: int, channel_id: int) -> bool:
return True
def apply_manual_result(guild_id: int, channel_id: int, kind: str,
opponent: str = "DEV") -> Optional["Tally"]:
"""Manually bump a VC's active tally by a win or loss (dev/testing).
``kind`` is "win" or "loss". Mirrors what a finished game would do but with
no real opponent, so ``last_opponent`` is set to a fixed label. Returns the
updated tally, or None if the VC has no active tally.
"""
tly = _REGISTRY.get((guild_id, channel_id))
if tly is None:
return None
if kind == "win":
tly.wins += 1
else:
tly.losses += 1
tly.last_result_kind = kind
tly.last_opponent = opponent
tly.last_update_ts = time.time()
_save()
return tly
# ---------------------------------------------------------------------------
# Voice-status HTTP helper
# ---------------------------------------------------------------------------
@@ -254,3 +286,21 @@ async def on_session_processed(guild_id: int, teams: list[dict],
await push_status(tly)
except Exception as e:
logging.error(f"[TALLY] on_session_processed error (guild={guild_id}): {e}")
# ---------------------------------------------------------------------------
# Idle sweep
# ---------------------------------------------------------------------------
async def sweep_idle() -> None:
"""Wipe tallies that are idle >= IDLE_TIMEOUT or whose channel is gone."""
now = time.time()
bot = get_bot()
for (g, c), tly in list(_REGISTRY.items()):
channel_gone = bot is None or bot.get_channel(c) is None
if channel_gone:
wipe(g, c) # channel gone: drop without an HTTP clear
continue
if now - tly.last_update_ts >= IDLE_TIMEOUT:
wipe(g, c)
await clear_status(c)