botWebWars/botagent/bot_agent.py

229 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""External Bot Agent with deliberate decision-making logic:
- Evaluates targets via Radar sensor.
- 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: move into adjacent square to trigger engine merge
self._pass_or_step()
except Exception as e:
print(f"Party formation error: {e}")
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 _step_or_attack(self, target: Dict[str, Any]):
"""Move adjacent/towards the target."""
moves_res = requests.get(f"{BASE_URL}/players/{self.bot_id}/available-moves").json()
available = [d for d, chk in moves_res["moves"].items() if chk["available"]]
if available:
# Move towards target
chosen = available[0]
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:
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 or explore unvisited areas."""
rec_dir = radar_res.get("recommended_direction")
moves_res = requests.get(f"{BASE_URL}/players/{self.bot_id}/available-moves").json()
available = [d for d, chk in moves_res["moves"].items() if chk["available"]]
if not available:
print("🚫 No available moves. Passing turn.")
requests.post(f"{BASE_URL}/players/{self.bot_id}/pass")
return
chosen_dir = rec_dir if rec_dir in available else 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()
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()