Smart hit assignation and chain monitoring
This commit is contained in:
Binary file not shown.
@@ -37,3 +37,6 @@ REASSIGN_DELAY = _config.get("REASSIGN_DELAY", 120)
|
||||
# Bot Assignment Settings
|
||||
ASSIGNMENT_TIMEOUT = _config.get("ASSIGNMENT_TIMEOUT", 60) # Seconds before reassigning a target
|
||||
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
|
||||
|
||||
8
main.py
8
main.py
@@ -17,9 +17,8 @@ from services.bot_assignment import BotAssignmentManager
|
||||
from routers import pages, factions, assignments, discord_mappings, config
|
||||
from routers import bot as bot_router
|
||||
|
||||
# ============================================================
|
||||
|
||||
# FastAPI Setup
|
||||
# ============================================================
|
||||
app = FastAPI()
|
||||
|
||||
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(config.router)
|
||||
|
||||
# ============================================================
|
||||
|
||||
# Discord Bot Setup
|
||||
# ============================================================
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
|
||||
@@ -96,9 +94,7 @@ async def start_bot():
|
||||
except Exception as e:
|
||||
print(f"ERROR starting Discord bot: {e}")
|
||||
|
||||
# ============================================================
|
||||
# Main Entry Point
|
||||
# ============================================================
|
||||
async def main():
|
||||
# Start Discord bot in background
|
||||
bot_task = asyncio.create_task(start_bot())
|
||||
|
||||
Binary file not shown.
@@ -42,7 +42,8 @@ async def get_config():
|
||||
"HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL,
|
||||
"REASSIGN_DELAY": config_module.REASSIGN_DELAY,
|
||||
"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:
|
||||
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,
|
||||
"REASSIGN_DELAY": config_module.REASSIGN_DELAY,
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,11 +1,12 @@
|
||||
# services/bot_assignment.py
|
||||
import asyncio
|
||||
import json
|
||||
import aiohttp
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
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:
|
||||
def __init__(self, bot):
|
||||
@@ -15,6 +16,14 @@ class BotAssignmentManager:
|
||||
self.running = False
|
||||
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
|
||||
self.load_discord_mapping()
|
||||
|
||||
@@ -38,6 +47,27 @@ class BotAssignmentManager:
|
||||
"""Get Discord user ID for a Torn player 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):
|
||||
"""Start the bot assignment loop"""
|
||||
if self.running:
|
||||
@@ -119,10 +149,125 @@ class BotAssignmentManager:
|
||||
return eid
|
||||
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):
|
||||
"""Main loop that assigns targets and monitors status"""
|
||||
"""Main loop that monitors chain timer and assigns targets"""
|
||||
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
|
||||
while self.running:
|
||||
@@ -136,31 +281,18 @@ class BotAssignmentManager:
|
||||
continue
|
||||
|
||||
if first_run:
|
||||
print("Bot activated - processing assignments")
|
||||
print("Bot activated - chain timer monitoring enabled")
|
||||
first_run = False
|
||||
|
||||
# Process each group
|
||||
has_assignments = False
|
||||
async with STATE.lock:
|
||||
for group_id, assignments in STATE.groups.items():
|
||||
friendly_ids = assignments.get("friendly", [])
|
||||
enemy_ids = assignments.get("enemy", [])
|
||||
# Check chain timer every 30 seconds
|
||||
now = datetime.now()
|
||||
if (now - self.last_chain_check).total_seconds() >= 30:
|
||||
await self.check_chain_timer()
|
||||
self.last_chain_check = now
|
||||
|
||||
if friendly_ids and enemy_ids:
|
||||
has_assignments = True
|
||||
|
||||
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!")
|
||||
# If chain is active, try to assign next hit
|
||||
if self.chain_active:
|
||||
await self.assign_next_chain_hit()
|
||||
|
||||
# Monitor active targets for status changes or timeouts
|
||||
await self.monitor_active_targets()
|
||||
|
||||
@@ -28,6 +28,9 @@ class ServerState:
|
||||
# bot running flag
|
||||
self.bot_running: bool = False
|
||||
|
||||
# faction IDs for API monitoring
|
||||
self.friendly_faction_id: Optional[int] = None
|
||||
|
||||
# concurrency lock for async safety
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ from config import TORN_API_KEY
|
||||
from .ffscouter import fetch_batch_stats
|
||||
from .server_state import STATE
|
||||
|
||||
# -----------------------------
|
||||
|
||||
# Tasks
|
||||
# -----------------------------
|
||||
friendly_status_task = None
|
||||
enemy_status_task = None
|
||||
|
||||
@@ -15,9 +14,8 @@ enemy_status_task = None
|
||||
friendly_lock = asyncio.Lock()
|
||||
enemy_lock = asyncio.Lock()
|
||||
|
||||
# -----------------------------
|
||||
|
||||
# Populate faction (memory only)
|
||||
# -----------------------------
|
||||
async def populate_faction(faction_id: int, kind: str):
|
||||
"""
|
||||
Fetch members + FFScouter estimates once and store in STATE.
|
||||
@@ -64,9 +62,8 @@ async def populate_faction(faction_id: int, kind: str):
|
||||
|
||||
return True
|
||||
|
||||
# -----------------------------
|
||||
|
||||
# Status refresh loop
|
||||
# -----------------------------
|
||||
async def refresh_status_loop(faction_id: int, kind: str, lock: asyncio.Lock, interval: int):
|
||||
"""
|
||||
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)
|
||||
|
||||
# -----------------------------
|
||||
|
||||
# Public API helpers
|
||||
# -----------------------------
|
||||
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")
|
||||
|
||||
async def populate_enemy(faction_id: int):
|
||||
|
||||
@@ -8,7 +8,8 @@ const CONFIG_FIELDS = {
|
||||
"HIT_CHECK_INTERVAL": "hit-check-interval",
|
||||
"REASSIGN_DELAY": "reassign-delay",
|
||||
"ASSIGNMENT_TIMEOUT": "assignment-timeout",
|
||||
"ASSIGNMENT_REMINDER": "assignment-reminder"
|
||||
"ASSIGNMENT_REMINDER": "assignment-reminder",
|
||||
"CHAIN_TIMER_THRESHOLD": "chain-timer-threshold"
|
||||
};
|
||||
|
||||
let sensitiveFields = [];
|
||||
|
||||
@@ -91,6 +91,17 @@
|
||||
<button class="config-save-btn" data-key="REASSIGN_DELAY">Save</button>
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user