2026-09-06 00:32:44 +00:00
""" External Bot Agent with deliberate decision-making logic:
- Evaluates targets via Radar sensor .
2026-09-06 01:04:12 +00:00
- 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 ) :
2026-09-06 13:29:31 +00:00
""" Register the bot avatar on the 64x64 grid or reconnect if already present. """
try :
players = requests . get ( f " { BASE_URL } /players " ) . json ( )
for p in players :
if p . get ( " name " ) == self . name :
self . bot_id = p [ " id " ]
print ( f " 🔄 [RECONNECT] Reconnected to existing { self . name } (ID: { self . bot_id } , Str: { p . get ( ' strength ' , self . strength ) } ) at ( { p . get ( ' x ' ) } , { p . get ( ' y ' ) } ) " )
return
except Exception :
pass
2026-09-06 00:32:44 +00:00
res = requests . post (
f " { BASE_URL } /players " ,
json = { " name " : self . name , " color " : self . color , " strength " : self . strength } ,
)
2026-09-06 13:29:31 +00:00
if res . status_code == 400 and " already registered " in res . text :
players = requests . get ( f " { BASE_URL } /players " ) . json ( )
for p in players :
if p . get ( " name " ) == self . name :
self . bot_id = p [ " id " ]
print ( f " 🔄 [RECONNECT] Reconnected to existing { self . name } (ID: { self . bot_id } ) at ( { p . get ( ' x ' ) } , { p . get ( ' y ' ) } ) " )
return
2026-09-06 00:32:44 +00:00
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 :
2026-09-06 01:04:12 +00:00
# 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 } " )
2026-09-06 01:04:12 +00:00
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 } " )
2026-09-06 01:04:12 +00:00
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 ] ) :
2026-09-06 01:04:12 +00:00
""" 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 ( )
2026-09-06 01:04:12 +00:00
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 :
2026-09-06 01:04:12 +00:00
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 ] ) :
2026-09-06 01:04:12 +00:00
""" Move towards the nearest target routing around obstacles. """
2026-09-06 00:32:44 +00:00
rec_dir = radar_res . get ( " recommended_direction " )
2026-09-06 01:04:12 +00:00
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 ( )
2026-09-06 01:04:12 +00:00
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 :
2026-09-06 01:04:12 +00:00
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
2026-09-06 01:04:12 +00:00
# 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 ( )
2026-09-06 13:24:06 +00:00
if not turn_info . get ( " game_started " , False ) :
2026-09-06 13:29:31 +00:00
print ( " ⏳ [LOBBY] Waiting for game to start via ' Start Game ' in UI... " , end = " \r " , flush = True )
2026-09-06 13:24:06 +00:00
time . sleep ( 0.5 )
continue
2026-09-06 00:32:44 +00:00
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 " \n Disconnecting { self . name } ... " )
requests . delete ( f " { BASE_URL } /players/ { self . bot_id } " )
if __name__ == " __main__ " :
agent = SmartBotAgent ( name = " ExternalCyberBot " , color = " #10b981 " , strength = 4 )
agent . run ( )