From b70789c3061260ef9ea8737b952202d37ef8dcd5 Mon Sep 17 00:00:00 2001 From: Isaac Johnson Date: Sun, 6 Sep 2026 08:32:14 -0500 Subject: [PATCH] feat(botagent): support CLI flags and environment variables for URL, name, color, and strength --- botagent/bot_agent.py | 119 +++++++++++++++++++++++++++++++++--------- 1 file changed, 94 insertions(+), 25 deletions(-) diff --git a/botagent/bot_agent.py b/botagent/bot_agent.py index a317dc8..3139377 100644 --- a/botagent/bot_agent.py +++ b/botagent/bot_agent.py @@ -4,20 +4,42 @@ - Decides whether to negotiate party formation or refuse & fight based on strength. - Explicitly engages in 3-bout D20 battles when adjacent to an opposing party/refusing bot. - Parses battle results, bout rolls, scores, and absorbed members. +- Configurable via CLI arguments or environment variables (URL, name, color, strength). """ +import os +import sys import time +import argparse import requests from typing import Optional, Dict, Any -BASE_URL = "http://localhost:8000/api" +DEFAULT_SERVER_URL = "http://localhost:8000/api" +DEFAULT_BOT_NAME = "ExternalCyberBot" +DEFAULT_BOT_COLOR = "#10b981" +DEFAULT_BOT_STRENGTH = 4 + + +def normalize_url(url: str) -> str: + """Ensure the API URL ends with /api without trailing slashes.""" + cleaned = url.rstrip("/") + if not cleaned.endswith("/api"): + cleaned = f"{cleaned}/api" + return cleaned class SmartBotAgent: - def __init__(self, name: str = "ExternalCyberBot", color: str = "#10b981", strength: int = 4): + def __init__( + self, + name: str = DEFAULT_BOT_NAME, + color: str = DEFAULT_BOT_COLOR, + strength: int = DEFAULT_BOT_STRENGTH, + server_url: str = DEFAULT_SERVER_URL, + ): self.name = name self.color = color self.strength = strength + self.base_url = normalize_url(server_url) self.bot_id: Optional[str] = None self.party_id: Optional[str] = None self.is_leader: bool = False @@ -25,7 +47,7 @@ class SmartBotAgent: def register(self): """Register the bot avatar on the 64x64 grid or reconnect if already present.""" try: - players = requests.get(f"{BASE_URL}/players").json() + players = requests.get(f"{self.base_url}/players").json() for p in players: if p.get("name") == self.name: self.bot_id = p["id"] @@ -35,11 +57,11 @@ class SmartBotAgent: pass res = requests.post( - f"{BASE_URL}/players", + f"{self.base_url}/players", json={"name": self.name, "color": self.color, "strength": self.strength}, ) if res.status_code == 400 and "already registered" in res.text: - players = requests.get(f"{BASE_URL}/players").json() + players = requests.get(f"{self.base_url}/players").json() for p in players: if p.get("name") == self.name: self.bot_id = p["id"] @@ -53,7 +75,7 @@ class SmartBotAgent: def refresh_status(self): """Update bot state (party membership, leader status, score).""" - res = requests.get(f"{BASE_URL}/players/{self.bot_id}") + res = requests.get(f"{self.base_url}/players/{self.bot_id}") if res.status_code == 200: data = res.json() self.party_id = data.get("party_id") @@ -70,7 +92,7 @@ class SmartBotAgent: print(f"\nšŸŽ® --- Turn for {self.name} | Score: {my_info['score']} | Party: {self.party_id or 'Solo'} ---") # 1. Consult Radar Sensor - radar_res = requests.get(f"{BASE_URL}/players/{self.bot_id}/radar").json() + radar_res = requests.get(f"{self.base_url}/players/{self.bot_id}/radar").json() targets = radar_res.get("targets", []) nearest = radar_res.get("nearest_target") @@ -113,9 +135,9 @@ class SmartBotAgent: # Leadership Rule: Desires equal or stronger leader. Refuses weaker leader! # Fetch target's party leader target_leader_str = target_str # default fallback - party_info = requests.get(f"{BASE_URL}/parties/{target_party}").json() + party_info = requests.get(f"{self.base_url}/parties/{target_party}").json() if party_info: - leader_player = requests.get(f"{BASE_URL}/players/{party_info['leader_id']}").json() + leader_player = requests.get(f"{self.base_url}/players/{party_info['leader_id']}").json() target_leader_str = leader_player.get("strength", 1) if self.strength <= target_leader_str: @@ -130,7 +152,7 @@ class SmartBotAgent: else: if not self.is_leader: print(f"šŸ›”ļø [PARTY MEMBER] Under command of party leader. Awaiting leader movement.") - requests.post(f"{BASE_URL}/players/{self.bot_id}/pass") + requests.post(f"{self.base_url}/players/{self.bot_id}/pass") return if not target_party: @@ -150,7 +172,7 @@ class SmartBotAgent: """Form a party using the REST API.""" try: res = requests.post( - f"{BASE_URL}/parties", + f"{self.base_url}/parties", json={"member_ids": member_ids, "leader_id": leader_id, "name": f"Squad_{self.name}"}, ) if res.status_code == 201: @@ -158,16 +180,16 @@ class SmartBotAgent: print(f"āœ… [PARTY FORMED] Squad '{party['name']}' established! Leader: {party['leader_name']} | Str: {party['total_strength']}") else: # Fallback: pass turn if party creation rejected - requests.post(f"{BASE_URL}/players/{self.bot_id}/pass") + requests.post(f"{self.base_url}/players/{self.bot_id}/pass") except Exception as e: print(f"Party formation error: {e}") - requests.post(f"{BASE_URL}/players/{self.bot_id}/pass") + requests.post(f"{self.base_url}/players/{self.bot_id}/pass") def _initiate_battle(self, opponent_id: str): """Explicitly call the 3-Bout D20 Battle endpoint.""" print(f"šŸŽ² [BATTLE INITIATED] Clashing with opponent {opponent_id}...") res = requests.post( - f"{BASE_URL}/battles/fight", + f"{self.base_url}/battles/fight", json={"challenger_id": self.bot_id, "defender_id": opponent_id}, ) if res.status_code == 200: @@ -197,31 +219,31 @@ class SmartBotAgent: def _step_or_attack(self, target: Dict[str, Any]): """Move adjacent/towards the target while avoiding obstacles.""" - moves_res = requests.get(f"{BASE_URL}/players/{self.bot_id}/available-moves").json() + moves_res = requests.get(f"{self.base_url}/players/{self.bot_id}/available-moves").json() moves = moves_res.get("moves", {}) chosen = self._get_best_move_towards(target["x"], target["y"], moves) if chosen: - res = requests.post(f"{BASE_URL}/players/{self.bot_id}/move", json={"direction": chosen}).json() + res = requests.post(f"{self.base_url}/players/{self.bot_id}/move", json={"direction": chosen}).json() if res.get("battle_triggered"): print(f"āš”ļø Move triggered battle! Winner: {res['battle_result']['winner_party_name']}") elif res.get("party_formed_triggered"): print(f"šŸ¤ Move resulted in party alliance!") else: print("āš ļø No passable moves adjacent to target (terrain/border constraint). Passing turn.") - requests.post(f"{BASE_URL}/players/{self.bot_id}/pass") + requests.post(f"{self.base_url}/players/{self.bot_id}/pass") def _navigate_towards_goal(self, radar_res: Dict[str, Any]): """Move towards the nearest target routing around obstacles.""" rec_dir = radar_res.get("recommended_direction") nearest = radar_res.get("nearest_target") - moves_res = requests.get(f"{BASE_URL}/players/{self.bot_id}/available-moves").json() + moves_res = requests.get(f"{self.base_url}/players/{self.bot_id}/available-moves").json() moves = moves_res.get("moves", {}) available = [d for d, chk in moves.items() if chk.get("available")] if not available: print("🚫 All adjacent paths blocked by borders or obstacle terrain (mountains/forests). Passing turn.") - requests.post(f"{BASE_URL}/players/{self.bot_id}/pass") + requests.post(f"{self.base_url}/players/{self.bot_id}/pass") return # 1. Prefer radar's obstacle-aware BFS pathfinder direction @@ -235,7 +257,7 @@ class SmartBotAgent: chosen_dir = available[0] print(f"🧭 Moving {chosen_dir} (Goal: {radar_res.get('bot_goal')}, Action: {radar_res.get('recommended_action')})") - res = requests.post(f"{BASE_URL}/players/{self.bot_id}/move", json={"direction": chosen_dir}).json() + res = requests.post(f"{self.base_url}/players/{self.bot_id}/move", json={"direction": chosen_dir}).json() if res.get("battle_triggered"): print(f"āš”ļø Encounter battle! Winner: {res['battle_result']['winner_party_name']}") @@ -247,7 +269,7 @@ class SmartBotAgent: self.register() try: while True: - turn_info = requests.get(f"{BASE_URL}/turn").json() + turn_info = requests.get(f"{self.base_url}/turn").json() if not turn_info.get("game_started", False): print("ā³ [LOBBY] Waiting for game to start via 'Start Game' in UI...", end="\r", flush=True) time.sleep(0.5) @@ -259,7 +281,7 @@ class SmartBotAgent: self.decide_and_act() # Check for game conclusion - conc = requests.get(f"{BASE_URL}/game/conclusion").json() + conc = requests.get(f"{self.base_url}/game/conclusion").json() if conc.get("concluded"): print(f"\nšŸŽ‰ [GAME CONCLUDED] All bots united under '{conc['winning_party_name']}'!") break @@ -268,9 +290,56 @@ class SmartBotAgent: except KeyboardInterrupt: print(f"\nDisconnecting {self.name}...") - requests.delete(f"{BASE_URL}/players/{self.bot_id}") + requests.delete(f"{self.base_url}/players/{self.bot_id}") + + +def main(): + # Read defaults from environment variables if present + env_url = os.environ.get("BOT_SERVER_URL") or os.environ.get("SERVER_URL") or DEFAULT_SERVER_URL + env_name = os.environ.get("BOT_NAME", DEFAULT_BOT_NAME) + env_color = os.environ.get("BOT_COLOR", DEFAULT_BOT_COLOR) + env_strength = int(os.environ.get("BOT_STRENGTH", str(DEFAULT_BOT_STRENGTH))) + + parser = argparse.ArgumentParser( + description="Autonomous External Bot Agent for botWebWars", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "-u", "--url", + dest="server_url", + default=env_url, + help="Backend REST API base URL (env: BOT_SERVER_URL or SERVER_URL)", + ) + parser.add_argument( + "-n", "--name", + dest="name", + default=env_name, + help="Display name for this bot (env: BOT_NAME)", + ) + parser.add_argument( + "-c", "--color", + dest="color", + default=env_color, + help="Hex color code for the bot avatar, e.g. #10b981 (env: BOT_COLOR)", + ) + parser.add_argument( + "-s", "--strength", + dest="strength", + type=int, + default=env_strength, + help="Strength attribute (1-10) for D20 battle multiplier (env: BOT_STRENGTH)", + ) + + args = parser.parse_args() + + agent = SmartBotAgent( + name=args.name, + color=args.color, + strength=args.strength, + server_url=args.server_url, + ) + agent.run() if __name__ == "__main__": - agent = SmartBotAgent(name="ExternalCyberBot", color="#10b981", strength=4) - agent.run() + main()