botWebWars/botagent/bot_agent.py

254 lines
12 KiB
Python
Raw Normal View History

2026-09-06 00:32:44 +00:00
"""External Bot Agent with deliberate decision-making logic:
- Evaluates targets via Radar sensor.
- Intelligently navigates around impassable obstacles (mountains and forests).
2026-09-06 00:32:44 +00:00
- 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.
"""
import time
import requests
from typing import Optional, Dict, Any
BASE_URL = "http://localhost:8000/api"
class SmartBotAgent:
def __init__(self, name: str = "ExternalCyberBot", color: str = "#10b981", strength: int = 4):
self.name = name
self.color = color
self.strength = strength
self.bot_id: Optional[str] = None
self.party_id: Optional[str] = None
self.is_leader: bool = False
def register(self):
"""Register the bot avatar on the 64x64 grid."""
res = requests.post(
f"{BASE_URL}/players",
json={"name": self.name, "color": self.color, "strength": self.strength},
)
res.raise_for_status()
data = res.json()
self.bot_id = data["id"]
print(f"🚀 [REGISTER] Spawned {self.name} (ID: {self.bot_id}, Str: {self.strength}) at ({data['x']}, {data['y']})")
def refresh_status(self):
"""Update bot state (party membership, leader status, score)."""
res = requests.get(f"{BASE_URL}/players/{self.bot_id}")
if res.status_code == 200:
data = res.json()
self.party_id = data.get("party_id")
self.is_leader = data.get("is_party_leader", False)
return data
return None
def decide_and_act(self):
"""Core AI decision loop executed when it is this bot's turn."""
my_info = self.refresh_status()
if not my_info:
return
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()
targets = radar_res.get("targets", [])
nearest = radar_res.get("nearest_target")
# 2. Check for immediate adjacent interaction (distance <= 1)
adjacent_target = None
for t in targets:
if t["distance"] <= 1 and not t["is_ally"]:
adjacent_target = t
break
if adjacent_target:
self._handle_adjacent_encounter(adjacent_target)
return
# 3. If no immediate adjacent enemy/recruit, move towards target
self._navigate_towards_goal(radar_res)
def _handle_adjacent_encounter(self, target: Dict[str, Any]):
"""Deliberate decision: Should we join, recruit, or fight?"""
target_name = target["name"]
target_str = target["strength"]
target_party = target.get("party_id")
print(f"🔍 [ADJACENT ENCOUNTER] Next to '{target_name}' (Str: {target_str}, Party: {target_party or 'None'})")
# SCENARIO A: I am a Solo Bot
if not self.party_id:
if not target_party:
# Both solo: agree to form a party! Stronger bot is leader.
if self.strength >= target_str:
print(f"🤝 [DECISION] Proposing party with {target_name}. I have >= strength, so I will lead!")
leader_id = self.bot_id
else:
print(f"🤝 [DECISION] Proposing party with {target_name}. They are stronger, so they will lead.")
leader_id = target["id"]
self._execute_party_formation([self.bot_id, target["id"]], leader_id)
else:
# Target belongs to a party: Check leader strength
# 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()
if party_info:
leader_player = requests.get(f"{BASE_URL}/players/{party_info['leader_id']}").json()
target_leader_str = leader_player.get("strength", 1)
if self.strength <= target_leader_str:
print(f"🤝 [DECISION] Party leader {party_info['leader_name']} has strength {target_leader_str} >= my {self.strength}. Willingly joining squad!")
# Step towards or engine auto-merges
self._step_or_attack(target)
else:
print(f"⚔️ [DECISION] Party leader has lower strength ({target_leader_str} < my {self.strength})! I REFUSE to join. Engaging in battle!")
self._initiate_battle(target["id"])
# SCENARIO B: I am in a Party
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")
return
if not target_party:
# Party Leader vs Solo Bot:
if target_str <= self.strength:
print(f"🤝 [DECISION] Solo bot {target_name} is willing to join our squad under my leadership.")
self._step_or_attack(target)
else:
print(f"⚔️ [DECISION] Solo bot {target_name} refuses weaker leader! Squad is attacking!")
self._initiate_battle(target["id"])
else:
# Party vs Opposing Party: BATTLE!
print(f"⚔️ [DECISION] Hostile party detected: '{target.get('party_name')}'! Engaging in 3-Bout D20 battle!")
self._initiate_battle(target["id"])
def _execute_party_formation(self, member_ids, leader_id):
"""Form a party using the REST API."""
try:
res = requests.post(
f"{BASE_URL}/parties",
json={"member_ids": member_ids, "leader_id": leader_id, "name": f"Squad_{self.name}"},
)
if res.status_code == 201:
party = res.json()
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")
2026-09-06 00:32:44 +00:00
except Exception as e:
print(f"Party formation error: {e}")
requests.post(f"{BASE_URL}/players/{self.bot_id}/pass")
2026-09-06 00:32:44 +00:00
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",
json={"challenger_id": self.bot_id, "defender_id": opponent_id},
)
if res.status_code == 200:
battle = res.json()
print(f"\n--- ⚔️ 3-BOUT BATTLE RESOLUTION ---")
print(f"Bouts won: {battle['party1_name']} ({battle['party1_bouts_won']}) vs {battle['party2_name']} ({battle['party2_bouts_won']})")
for b in battle["bouts"]:
print(f" Bout #{b['bout_number']}: Roll {b['party1_roll']}×{b['party1_strength']} ({b['party1_score']}) vs Roll {b['party2_roll']}×{b['party2_strength']} ({b['party2_score']}) -> Winner: {b['winner_name']}")
print(f"🏆 Overall Winner: {battle['winner_party_name']} (Leader {battle['winner_leader_name']} receives +2 pts)")
print(f"💀 Defeated: {battle['defeated_party_name']} (Leader {battle['killed_leader_name']} -1 pt)")
if battle.get("absorbed_members"):
print(f"🧲 Absorbed {len(battle['absorbed_members'])} member(s) into {battle['winner_party_name']}")
else:
print(f"Battle failed ({res.status_code}): {res.text}")
def _get_best_move_towards(self, target_x: int, target_y: int, moves: Dict[str, Any]) -> Optional[str]:
"""Pick the available direction that minimizes Chebyshev distance to (target_x, target_y), strictly avoiding obstacles."""
valid_moves = {d: chk for d, chk in moves.items() if chk.get("available")}
if not valid_moves:
return None
def dist(chk: Dict[str, Any]) -> int:
return max(abs(chk["target_x"] - target_x), abs(chk["target_y"] - target_y))
return min(valid_moves.keys(), key=lambda d: dist(valid_moves[d]))
2026-09-06 00:32:44 +00:00
def _step_or_attack(self, target: Dict[str, Any]):
"""Move adjacent/towards the target while avoiding obstacles."""
2026-09-06 00:32:44 +00:00
moves_res = requests.get(f"{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:
2026-09-06 00:32:44 +00:00
res = requests.post(f"{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.")
2026-09-06 00:32:44 +00:00
requests.post(f"{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."""
2026-09-06 00:32:44 +00:00
rec_dir = radar_res.get("recommended_direction")
nearest = radar_res.get("nearest_target")
2026-09-06 00:32:44 +00:00
moves_res = requests.get(f"{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")]
2026-09-06 00:32:44 +00:00
if not available:
print("🚫 All adjacent paths blocked by borders or obstacle terrain (mountains/forests). Passing turn.")
2026-09-06 00:32:44 +00:00
requests.post(f"{BASE_URL}/players/{self.bot_id}/pass")
return
# 1. Prefer radar's obstacle-aware BFS pathfinder direction
if rec_dir and rec_dir in available:
chosen_dir = rec_dir
# 2. Otherwise pick the available direction that minimizes distance to the nearest target
elif nearest:
chosen_dir = self._get_best_move_towards(nearest["x"], nearest["y"], moves) or available[0]
# 3. Fallback to any valid open terrain cell
else:
chosen_dir = available[0]
2026-09-06 00:32:44 +00:00
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()
if res.get("battle_triggered"):
print(f"⚔️ Encounter battle! Winner: {res['battle_result']['winner_party_name']}")
elif res.get("party_formed_triggered"):
print(f"🤝 Formed or joined squad: {res.get('formed_party', {}).get('name')}")
def run(self):
"""Main game loop for external agent."""
self.register()
try:
while True:
turn_info = requests.get(f"{BASE_URL}/turn").json()
curr_player_id = turn_info.get("current_player_id")
if curr_player_id == self.bot_id:
self.decide_and_act()
# Check for game conclusion
conc = requests.get(f"{BASE_URL}/game/conclusion").json()
if conc.get("concluded"):
print(f"\n🎉 [GAME CONCLUDED] All bots united under '{conc['winning_party_name']}'!")
break
else:
time.sleep(0.4)
except KeyboardInterrupt:
print(f"\nDisconnecting {self.name}...")
requests.delete(f"{BASE_URL}/players/{self.bot_id}")
if __name__ == "__main__":
agent = SmartBotAgent(name="ExternalCyberBot", color="#10b981", strength=4)
agent.run()