Smart hit assignation and chain monitoring

This commit is contained in:
2026-01-27 08:26:48 -05:00
parent 5ef8707122
commit 99ffe7f9e9
13 changed files with 188 additions and 42 deletions

Binary file not shown.

View File

@@ -37,3 +37,6 @@ REASSIGN_DELAY = _config.get("REASSIGN_DELAY", 120)
# Bot Assignment Settings # Bot Assignment Settings
ASSIGNMENT_TIMEOUT = _config.get("ASSIGNMENT_TIMEOUT", 60) # Seconds before reassigning a target ASSIGNMENT_TIMEOUT = _config.get("ASSIGNMENT_TIMEOUT", 60) # Seconds before reassigning a target
ASSIGNMENT_REMINDER = _config.get("ASSIGNMENT_REMINDER", 45) # Seconds before sending reminder message ASSIGNMENT_REMINDER = _config.get("ASSIGNMENT_REMINDER", 45) # Seconds before sending reminder message
# Chain Timer Settings
CHAIN_TIMER_THRESHOLD = _config.get("CHAIN_TIMER_THRESHOLD", 5) # Minutes - start assigning hits when chain timer is at or below this

View File

@@ -17,9 +17,8 @@ from services.bot_assignment import BotAssignmentManager
from routers import pages, factions, assignments, discord_mappings, config from routers import pages, factions, assignments, discord_mappings, config
from routers import bot as bot_router from routers import bot as bot_router
# ============================================================
# FastAPI Setup # FastAPI Setup
# ============================================================
app = FastAPI() app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
@@ -32,9 +31,8 @@ app.include_router(bot_router.router)
app.include_router(discord_mappings.router) app.include_router(discord_mappings.router)
app.include_router(config.router) app.include_router(config.router)
# ============================================================
# Discord Bot Setup # Discord Bot Setup
# ============================================================
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
@@ -96,9 +94,7 @@ async def start_bot():
except Exception as e: except Exception as e:
print(f"ERROR starting Discord bot: {e}") print(f"ERROR starting Discord bot: {e}")
# ============================================================
# Main Entry Point # Main Entry Point
# ============================================================
async def main(): async def main():
# Start Discord bot in background # Start Discord bot in background
bot_task = asyncio.create_task(start_bot()) bot_task = asyncio.create_task(start_bot())

View File

@@ -42,7 +42,8 @@ async def get_config():
"HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL, "HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL,
"REASSIGN_DELAY": config_module.REASSIGN_DELAY, "REASSIGN_DELAY": config_module.REASSIGN_DELAY,
"ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT, "ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT,
"ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER "ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER,
"CHAIN_TIMER_THRESHOLD": config_module.CHAIN_TIMER_THRESHOLD
} }
else: else:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
@@ -81,7 +82,8 @@ async def update_config(req: ConfigUpdateRequest):
"HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL, "HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL,
"REASSIGN_DELAY": config_module.REASSIGN_DELAY, "REASSIGN_DELAY": config_module.REASSIGN_DELAY,
"ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT, "ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT,
"ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER "ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER,
"CHAIN_TIMER_THRESHOLD": config_module.CHAIN_TIMER_THRESHOLD
} }
} }

View File

@@ -1,11 +1,12 @@
# services/bot_assignment.py # services/bot_assignment.py
import asyncio import asyncio
import json import json
import aiohttp
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
from datetime import datetime from datetime import datetime
from services.server_state import STATE from services.server_state import STATE
from config import ASSIGNMENT_TIMEOUT, ASSIGNMENT_REMINDER, ALLOWED_CHANNEL_ID from config import ASSIGNMENT_TIMEOUT, ASSIGNMENT_REMINDER, ALLOWED_CHANNEL_ID, CHAIN_TIMER_THRESHOLD, TORN_API_KEY
class BotAssignmentManager: class BotAssignmentManager:
def __init__(self, bot): def __init__(self, bot):
@@ -15,6 +16,14 @@ class BotAssignmentManager:
self.running = False self.running = False
self.task = None self.task = None
# Chain monitoring state
self.chain_timeout = 0 # Seconds remaining on chain
self.chain_active = False # Whether we're in assignment mode
self.assigned_friendlies = set() # Track who has been assigned in this chain session
self.current_group_index = 0 # For round-robin group assignment
self.chain_warning_sent = False # Track if 30-second warning was sent
self.last_chain_check = datetime.now()
# Load Discord mapping # Load Discord mapping
self.load_discord_mapping() self.load_discord_mapping()
@@ -38,6 +47,27 @@ class BotAssignmentManager:
"""Get Discord user ID for a Torn player ID""" """Get Discord user ID for a Torn player ID"""
return self.discord_mapping.get(torn_id) return self.discord_mapping.get(torn_id)
async def fetch_chain_timer(self) -> Optional[int]:
"""Fetch chain timeout from Torn API. Returns seconds remaining, or None if no chain or error."""
if not STATE.friendly_faction_id:
return None
try:
url = f"https://api.torn.com/v2/faction/{STATE.friendly_faction_id}/chain?key={TORN_API_KEY}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status != 200:
return None
data = await resp.json()
# Get timeout value in seconds
chain_data = data.get("chain", {})
timeout = chain_data.get("timeout", 0)
return timeout
except Exception as e:
print(f"Error fetching chain timer: {e}")
return None
async def start(self): async def start(self):
"""Start the bot assignment loop""" """Start the bot assignment loop"""
if self.running: if self.running:
@@ -119,10 +149,125 @@ class BotAssignmentManager:
return eid return eid
return None return None
async def check_chain_timer(self):
"""Check chain timer and update chain state"""
timeout = await self.fetch_chain_timer()
if timeout is None:
# No chain data or error
if self.chain_active:
print("Chain ended or API error - resetting chain state")
self.chain_active = False
self.assigned_friendlies.clear()
self.current_group_index = 0
self.chain_warning_sent = False
return
self.chain_timeout = timeout
threshold_seconds = CHAIN_TIMER_THRESHOLD * 60
# Check if we should enter chain mode
if timeout > 0 and timeout <= threshold_seconds and not self.chain_active:
print(f"Chain timer at {timeout}s (threshold: {threshold_seconds}s) - entering chain mode")
self.chain_active = True
self.assigned_friendlies.clear()
self.current_group_index = 0
self.chain_warning_sent = False
# Check if chain expired
elif timeout > threshold_seconds and self.chain_active:
print(f"Chain timer above threshold ({timeout}s > {threshold_seconds}s) - exiting chain mode")
self.chain_active = False
self.assigned_friendlies.clear()
self.current_group_index = 0
self.chain_warning_sent = False
# Check if chain expired (timeout = 0)
elif timeout == 0 and self.chain_active:
print("Chain expired - resetting chain state")
self.chain_active = False
self.assigned_friendlies.clear()
self.current_group_index = 0
self.chain_warning_sent = False
# Send 30-second warning
if self.chain_active and timeout <= 30 and not self.chain_warning_sent:
await self.send_chain_expiration_warning()
self.chain_warning_sent = True
async def send_chain_expiration_warning(self):
"""Send @here alert that chain is about to expire"""
try:
channel = self.bot.get_channel(ALLOWED_CHANNEL_ID)
if channel and STATE.friendly_faction_id:
faction_link = f"https://www.torn.com/factions.php?step=your#/tab=chain"
message = f"@here **CHAIN EXPIRING IN 30 SECONDS!** Attack a faction member to keep it alive!\n{faction_link}"
await channel.send(message)
print("Sent chain expiration warning")
except Exception as e:
print(f"Error sending chain warning: {e}")
async def assign_next_chain_hit(self):
"""Assign next hit in round-robin fashion through groups"""
# Only assign if there's no active assignment waiting
if self.active_targets:
return # Wait for current assignment to complete
# Get list of group IDs (sorted for consistency)
group_ids = sorted(STATE.groups.keys())
if not group_ids:
return
# Try to find a group with available friendly and enemy
attempts = 0
while attempts < len(group_ids):
# Get current group
group_id = group_ids[self.current_group_index % len(group_ids)]
async with STATE.lock:
friendly_ids = STATE.groups[group_id].get("friendly", [])
enemy_ids = STATE.groups[group_id].get("enemy", [])
# Find a friendly who hasn't been assigned yet
available_friendly = None
for fid in friendly_ids:
if fid not in self.assigned_friendlies:
available_friendly = fid
break
# Find an attackable enemy
enemy_id = self.get_next_enemy_in_group(group_id, enemy_ids)
if available_friendly and enemy_id:
# Make assignment
self.assigned_friendlies.add(available_friendly)
await self.assign_target(group_id, available_friendly, enemy_id)
print(f"Chain hit assigned: Group {group_id}, Friendly {available_friendly} -> Enemy {enemy_id}")
# Move to next group for next assignment
self.current_group_index += 1
return
# Move to next group and try again
self.current_group_index += 1
attempts += 1
# If we've tried all groups and no assignments possible
# Check if we need to reset assigned_friendlies (everyone has been assigned)
async with STATE.lock:
all_friendlies = set()
for assignments in STATE.groups.values():
all_friendlies.update(assignments.get("friendly", []))
if self.assigned_friendlies and self.assigned_friendlies >= all_friendlies:
print("All friendlies have been assigned - resetting for round-robin")
self.assigned_friendlies.clear()
self.current_group_index = 0
async def assignment_loop(self): async def assignment_loop(self):
"""Main loop that assigns targets and monitors status""" """Main loop that monitors chain timer and assigns targets"""
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
print("Bot is ready, assignment loop running") print("Bot is ready, assignment loop running with chain timer monitoring")
first_run = True first_run = True
while self.running: while self.running:
@@ -136,31 +281,18 @@ class BotAssignmentManager:
continue continue
if first_run: if first_run:
print("Bot activated - processing assignments") print("Bot activated - chain timer monitoring enabled")
first_run = False first_run = False
# Process each group # Check chain timer every 30 seconds
has_assignments = False now = datetime.now()
async with STATE.lock: if (now - self.last_chain_check).total_seconds() >= 30:
for group_id, assignments in STATE.groups.items(): await self.check_chain_timer()
friendly_ids = assignments.get("friendly", []) self.last_chain_check = now
enemy_ids = assignments.get("enemy", [])
if friendly_ids and enemy_ids: # If chain is active, try to assign next hit
has_assignments = True if self.chain_active:
await self.assign_next_chain_hit()
if not friendly_ids or not enemy_ids:
continue
# Try to assign any unassigned enemies
enemy_id = self.get_next_enemy_in_group(group_id, enemy_ids)
if enemy_id:
friendly_id = self.get_next_friendly_in_group(group_id, friendly_ids)
if friendly_id:
await self.assign_target(group_id, friendly_id, enemy_id)
if not has_assignments and STATE.bot_running:
print("No group assignments found - drag members into groups first!")
# Monitor active targets for status changes or timeouts # Monitor active targets for status changes or timeouts
await self.monitor_active_targets() await self.monitor_active_targets()

View File

@@ -28,6 +28,9 @@ class ServerState:
# bot running flag # bot running flag
self.bot_running: bool = False self.bot_running: bool = False
# faction IDs for API monitoring
self.friendly_faction_id: Optional[int] = None
# concurrency lock for async safety # concurrency lock for async safety
self.lock = asyncio.Lock() self.lock = asyncio.Lock()

View File

@@ -5,9 +5,8 @@ from config import TORN_API_KEY
from .ffscouter import fetch_batch_stats from .ffscouter import fetch_batch_stats
from .server_state import STATE from .server_state import STATE
# -----------------------------
# Tasks # Tasks
# -----------------------------
friendly_status_task = None friendly_status_task = None
enemy_status_task = None enemy_status_task = None
@@ -15,9 +14,8 @@ enemy_status_task = None
friendly_lock = asyncio.Lock() friendly_lock = asyncio.Lock()
enemy_lock = asyncio.Lock() enemy_lock = asyncio.Lock()
# -----------------------------
# Populate faction (memory only) # Populate faction (memory only)
# -----------------------------
async def populate_faction(faction_id: int, kind: str): async def populate_faction(faction_id: int, kind: str):
""" """
Fetch members + FFScouter estimates once and store in STATE. Fetch members + FFScouter estimates once and store in STATE.
@@ -64,9 +62,8 @@ async def populate_faction(faction_id: int, kind: str):
return True return True
# -----------------------------
# Status refresh loop # Status refresh loop
# -----------------------------
async def refresh_status_loop(faction_id: int, kind: str, lock: asyncio.Lock, interval: int): async def refresh_status_loop(faction_id: int, kind: str, lock: asyncio.Lock, interval: int):
""" """
Periodically refresh member statuses in STATE. Periodically refresh member statuses in STATE.
@@ -95,10 +92,11 @@ async def refresh_status_loop(faction_id: int, kind: str, lock: asyncio.Lock, in
await asyncio.sleep(interval) await asyncio.sleep(interval)
# -----------------------------
# Public API helpers # Public API helpers
# -----------------------------
async def populate_friendly(faction_id: int): async def populate_friendly(faction_id: int):
# Store friendly faction ID for chain monitoring
STATE.friendly_faction_id = faction_id
return await populate_faction(faction_id, "friendly") return await populate_faction(faction_id, "friendly")
async def populate_enemy(faction_id: int): async def populate_enemy(faction_id: int):

View File

@@ -8,7 +8,8 @@ const CONFIG_FIELDS = {
"HIT_CHECK_INTERVAL": "hit-check-interval", "HIT_CHECK_INTERVAL": "hit-check-interval",
"REASSIGN_DELAY": "reassign-delay", "REASSIGN_DELAY": "reassign-delay",
"ASSIGNMENT_TIMEOUT": "assignment-timeout", "ASSIGNMENT_TIMEOUT": "assignment-timeout",
"ASSIGNMENT_REMINDER": "assignment-reminder" "ASSIGNMENT_REMINDER": "assignment-reminder",
"CHAIN_TIMER_THRESHOLD": "chain-timer-threshold"
}; };
let sensitiveFields = []; let sensitiveFields = [];

View File

@@ -91,6 +91,17 @@
<button class="config-save-btn" data-key="REASSIGN_DELAY">Save</button> <button class="config-save-btn" data-key="REASSIGN_DELAY">Save</button>
</div> </div>
</div> </div>
<!-- Chain Timer Settings Section -->
<div class="faction-card small config-section">
<h2>Chain Timer Settings</h2>
<div class="config-group">
<label for="chain-timer-threshold">Chain Timer Threshold (minutes)</label>
<p class="config-description">Start assigning hits when chain timer is at or below this many minutes</p>
<input type="number" id="chain-timer-threshold" class="config-input" />
<button class="config-save-btn" data-key="CHAIN_TIMER_THRESHOLD">Save</button>
</div>
</div>
</div> </div>
</div> </div>