feat(botagent): support CLI flags and environment variables for URL, name, color, and strength
This commit is contained in:
parent
e9e95c409b
commit
b70789c306
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue