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.
|
- 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.
|
- Explicitly engages in 3-bout D20 battles when adjacent to an opposing party/refusing bot.
|
||||||
- Parses battle results, bout rolls, scores, and absorbed members.
|
- 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 time
|
||||||
|
import argparse
|
||||||
import requests
|
import requests
|
||||||
from typing import Optional, Dict, Any
|
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:
|
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.name = name
|
||||||
self.color = color
|
self.color = color
|
||||||
self.strength = strength
|
self.strength = strength
|
||||||
|
self.base_url = normalize_url(server_url)
|
||||||
self.bot_id: Optional[str] = None
|
self.bot_id: Optional[str] = None
|
||||||
self.party_id: Optional[str] = None
|
self.party_id: Optional[str] = None
|
||||||
self.is_leader: bool = False
|
self.is_leader: bool = False
|
||||||
|
|
@ -25,7 +47,7 @@ class SmartBotAgent:
|
||||||
def register(self):
|
def register(self):
|
||||||
"""Register the bot avatar on the 64x64 grid or reconnect if already present."""
|
"""Register the bot avatar on the 64x64 grid or reconnect if already present."""
|
||||||
try:
|
try:
|
||||||
players = requests.get(f"{BASE_URL}/players").json()
|
players = requests.get(f"{self.base_url}/players").json()
|
||||||
for p in players:
|
for p in players:
|
||||||
if p.get("name") == self.name:
|
if p.get("name") == self.name:
|
||||||
self.bot_id = p["id"]
|
self.bot_id = p["id"]
|
||||||
|
|
@ -35,11 +57,11 @@ class SmartBotAgent:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
res = requests.post(
|
res = requests.post(
|
||||||
f"{BASE_URL}/players",
|
f"{self.base_url}/players",
|
||||||
json={"name": self.name, "color": self.color, "strength": self.strength},
|
json={"name": self.name, "color": self.color, "strength": self.strength},
|
||||||
)
|
)
|
||||||
if res.status_code == 400 and "already registered" in res.text:
|
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:
|
for p in players:
|
||||||
if p.get("name") == self.name:
|
if p.get("name") == self.name:
|
||||||
self.bot_id = p["id"]
|
self.bot_id = p["id"]
|
||||||
|
|
@ -53,7 +75,7 @@ class SmartBotAgent:
|
||||||
|
|
||||||
def refresh_status(self):
|
def refresh_status(self):
|
||||||
"""Update bot state (party membership, leader status, score)."""
|
"""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:
|
if res.status_code == 200:
|
||||||
data = res.json()
|
data = res.json()
|
||||||
self.party_id = data.get("party_id")
|
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'} ---")
|
print(f"\n🎮 --- Turn for {self.name} | Score: {my_info['score']} | Party: {self.party_id or 'Solo'} ---")
|
||||||
|
|
||||||
# 1. Consult Radar Sensor
|
# 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", [])
|
targets = radar_res.get("targets", [])
|
||||||
nearest = radar_res.get("nearest_target")
|
nearest = radar_res.get("nearest_target")
|
||||||
|
|
||||||
|
|
@ -113,9 +135,9 @@ class SmartBotAgent:
|
||||||
# Leadership Rule: Desires equal or stronger leader. Refuses weaker leader!
|
# Leadership Rule: Desires equal or stronger leader. Refuses weaker leader!
|
||||||
# Fetch target's party leader
|
# Fetch target's party leader
|
||||||
target_leader_str = target_str # default fallback
|
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:
|
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)
|
target_leader_str = leader_player.get("strength", 1)
|
||||||
|
|
||||||
if self.strength <= target_leader_str:
|
if self.strength <= target_leader_str:
|
||||||
|
|
@ -130,7 +152,7 @@ class SmartBotAgent:
|
||||||
else:
|
else:
|
||||||
if not self.is_leader:
|
if not self.is_leader:
|
||||||
print(f"🛡️ [PARTY MEMBER] Under command of party leader. Awaiting leader movement.")
|
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
|
return
|
||||||
|
|
||||||
if not target_party:
|
if not target_party:
|
||||||
|
|
@ -150,7 +172,7 @@ class SmartBotAgent:
|
||||||
"""Form a party using the REST API."""
|
"""Form a party using the REST API."""
|
||||||
try:
|
try:
|
||||||
res = requests.post(
|
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}"},
|
json={"member_ids": member_ids, "leader_id": leader_id, "name": f"Squad_{self.name}"},
|
||||||
)
|
)
|
||||||
if res.status_code == 201:
|
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']}")
|
print(f"✅ [PARTY FORMED] Squad '{party['name']}' established! Leader: {party['leader_name']} | Str: {party['total_strength']}")
|
||||||
else:
|
else:
|
||||||
# Fallback: pass turn if party creation rejected
|
# 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:
|
except Exception as e:
|
||||||
print(f"Party formation error: {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):
|
def _initiate_battle(self, opponent_id: str):
|
||||||
"""Explicitly call the 3-Bout D20 Battle endpoint."""
|
"""Explicitly call the 3-Bout D20 Battle endpoint."""
|
||||||
print(f"🎲 [BATTLE INITIATED] Clashing with opponent {opponent_id}...")
|
print(f"🎲 [BATTLE INITIATED] Clashing with opponent {opponent_id}...")
|
||||||
res = requests.post(
|
res = requests.post(
|
||||||
f"{BASE_URL}/battles/fight",
|
f"{self.base_url}/battles/fight",
|
||||||
json={"challenger_id": self.bot_id, "defender_id": opponent_id},
|
json={"challenger_id": self.bot_id, "defender_id": opponent_id},
|
||||||
)
|
)
|
||||||
if res.status_code == 200:
|
if res.status_code == 200:
|
||||||
|
|
@ -197,31 +219,31 @@ class SmartBotAgent:
|
||||||
|
|
||||||
def _step_or_attack(self, target: Dict[str, Any]):
|
def _step_or_attack(self, target: Dict[str, Any]):
|
||||||
"""Move adjacent/towards the target while avoiding obstacles."""
|
"""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", {})
|
moves = moves_res.get("moves", {})
|
||||||
chosen = self._get_best_move_towards(target["x"], target["y"], moves)
|
chosen = self._get_best_move_towards(target["x"], target["y"], moves)
|
||||||
|
|
||||||
if chosen:
|
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"):
|
if res.get("battle_triggered"):
|
||||||
print(f"⚔️ Move triggered battle! Winner: {res['battle_result']['winner_party_name']}")
|
print(f"⚔️ Move triggered battle! Winner: {res['battle_result']['winner_party_name']}")
|
||||||
elif res.get("party_formed_triggered"):
|
elif res.get("party_formed_triggered"):
|
||||||
print(f"🤝 Move resulted in party alliance!")
|
print(f"🤝 Move resulted in party alliance!")
|
||||||
else:
|
else:
|
||||||
print("⚠️ No passable moves adjacent to target (terrain/border constraint). Passing turn.")
|
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]):
|
def _navigate_towards_goal(self, radar_res: Dict[str, Any]):
|
||||||
"""Move towards the nearest target routing around obstacles."""
|
"""Move towards the nearest target routing around obstacles."""
|
||||||
rec_dir = radar_res.get("recommended_direction")
|
rec_dir = radar_res.get("recommended_direction")
|
||||||
nearest = radar_res.get("nearest_target")
|
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", {})
|
moves = moves_res.get("moves", {})
|
||||||
available = [d for d, chk in moves.items() if chk.get("available")]
|
available = [d for d, chk in moves.items() if chk.get("available")]
|
||||||
|
|
||||||
if not available:
|
if not available:
|
||||||
print("🚫 All adjacent paths blocked by borders or obstacle terrain (mountains/forests). Passing turn.")
|
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
|
return
|
||||||
|
|
||||||
# 1. Prefer radar's obstacle-aware BFS pathfinder direction
|
# 1. Prefer radar's obstacle-aware BFS pathfinder direction
|
||||||
|
|
@ -235,7 +257,7 @@ class SmartBotAgent:
|
||||||
chosen_dir = available[0]
|
chosen_dir = available[0]
|
||||||
|
|
||||||
print(f"🧭 Moving {chosen_dir} (Goal: {radar_res.get('bot_goal')}, Action: {radar_res.get('recommended_action')})")
|
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"):
|
if res.get("battle_triggered"):
|
||||||
print(f"⚔️ Encounter battle! Winner: {res['battle_result']['winner_party_name']}")
|
print(f"⚔️ Encounter battle! Winner: {res['battle_result']['winner_party_name']}")
|
||||||
|
|
@ -247,7 +269,7 @@ class SmartBotAgent:
|
||||||
self.register()
|
self.register()
|
||||||
try:
|
try:
|
||||||
while True:
|
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):
|
if not turn_info.get("game_started", False):
|
||||||
print("⏳ [LOBBY] Waiting for game to start via 'Start Game' in UI...", end="\r", flush=True)
|
print("⏳ [LOBBY] Waiting for game to start via 'Start Game' in UI...", end="\r", flush=True)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
@ -259,7 +281,7 @@ class SmartBotAgent:
|
||||||
self.decide_and_act()
|
self.decide_and_act()
|
||||||
|
|
||||||
# Check for game conclusion
|
# 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"):
|
if conc.get("concluded"):
|
||||||
print(f"\n🎉 [GAME CONCLUDED] All bots united under '{conc['winning_party_name']}'!")
|
print(f"\n🎉 [GAME CONCLUDED] All bots united under '{conc['winning_party_name']}'!")
|
||||||
break
|
break
|
||||||
|
|
@ -268,9 +290,56 @@ class SmartBotAgent:
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print(f"\nDisconnecting {self.name}...")
|
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__":
|
if __name__ == "__main__":
|
||||||
agent = SmartBotAgent(name="ExternalCyberBot", color="#10b981", strength=4)
|
main()
|
||||||
agent.run()
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue