diff --git a/botagent_ai/INSTALL.md b/botagent_ai/INSTALL.md index 885e980..862e4b0 100644 --- a/botagent_ai/INSTALL.md +++ b/botagent_ai/INSTALL.md @@ -44,3 +44,9 @@ Run the script, then start the game from the frontend UI: ```bash python bot.py ``` + +## Example with local LLM + +``` +$ OLLAMA_BASE_URL="http://192.168.1.220:11434" python3 ./bot.py -n myAIBot -s 2 +``` \ No newline at end of file diff --git a/botagent_ai/bot.py b/botagent_ai/bot.py index b0be6b1..ae16747 100644 --- a/botagent_ai/bot.py +++ b/botagent_ai/bot.py @@ -340,6 +340,52 @@ Respond ONLY with JSON: {{"form_alliance": true|false, "reasoning": "short reaso elif res.get("party_formed_triggered"): print(f"🤝 Formed or joined squad: {res.get('formed_party', {}).get('name')}") + def _get_board_snapshot(self) -> Optional[Dict[str, Any]]: + """Fetch the full board (players, parties, obstacles) so the LLM sees more than radar's nearest few.""" + try: + res = requests.get(f"{self.base_url}/board", timeout=10) + res.raise_for_status() + return res.json() + except requests.exceptions.RequestException as e: + print(f"⚠️ [BOARD FETCH ERROR] {e}") + return None + + OBSTACLE_SYMBOLS = {"mountain": "M", "forest": "F", "valley": "V"} + LOCAL_MAP_RADIUS = 8 + + def _build_local_map(self, board: Dict[str, Any], center_x: int, center_y: int) -> List[str]: + """Render an ASCII minimap centered on the bot: @ = self, A = ally, E = enemy, M/F/V = obstacles, . = open.""" + config = board.get("config", {}) + max_x = config.get("max_x", 64) + max_y = config.get("max_y", 64) + radius = self.LOCAL_MAP_RADIUS + + obstacle_at = {(o["x"], o["y"]): o.get("type", "mountain") for o in board.get("obstacles", [])} + player_at: Dict[tuple, List[Dict[str, Any]]] = {} + for p in board.get("players", []): + if p["id"] == self.bot_id: + continue + player_at.setdefault((p["x"], p["y"]), []).append(p) + + rows = [] + for y in range(center_y - radius, center_y + radius + 1): + row_chars = [] + for x in range(center_x - radius, center_x + radius + 1): + if x == center_x and y == center_y: + row_chars.append("@") + elif x < 0 or y < 0 or x >= max_x or y >= max_y: + row_chars.append("#") + elif (x, y) in obstacle_at: + row_chars.append(self.OBSTACLE_SYMBOLS.get(obstacle_at[(x, y)], "M")) + elif (x, y) in player_at: + occupants = player_at[(x, y)] + is_ally = self.party_id and any(o.get("party_id") == self.party_id for o in occupants) + row_chars.append("A" if is_ally else "E") + else: + row_chars.append(".") + rows.append("".join(row_chars)) + return rows + def _ask_llm_for_direction( self, radar_res: Dict[str, Any], @@ -350,12 +396,14 @@ Respond ONLY with JSON: {{"form_alliance": true|false, "reasoning": "short reaso targets_summary = [ { "name": t["name"], + "x": t["x"], + "y": t["y"], "distance": t["distance"], "strength": t["strength"], - "is_party": bool(t.get("party_id")), + "party": t.get("party_name") or ("solo" if not t.get("party_id") else t.get("party_id")), "is_ally": t.get("is_ally", False), } - for t in radar_res.get("targets", [])[:6] + for t in radar_res.get("targets", []) ] moves_summary = { d: { @@ -367,17 +415,31 @@ Respond ONLY with JSON: {{"form_alliance": true|false, "reasoning": "short reaso if d in available } + board = self._get_board_snapshot() + map_section = "" + if board: + local_map = self._build_local_map(board, my_info["x"], my_info["y"]) + map_section = f""" +Local map (radius {self.LOCAL_MAP_RADIUS} around you, row = one Y line, top-to-bottom is +increasing Y, left-to-right is increasing X): @ = you, A = ally, E = enemy/neutral bot, +M = mountain, F = forest, V = valley, # = out of bounds, . = open ground. +{chr(10).join(local_map)} +""" + prompt = f"""{GAME_RULES_SUMMARY} -You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, party: {self.party_id or 'Solo'}). +You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, party: {self.party_id or 'Solo'}) +at position ({my_info['x']}, {my_info['y']}). Server's radar suggestion: recommended_direction={radar_res.get('recommended_direction')}, recommended_action={radar_res.get('recommended_action')}, goal={radar_res.get('bot_goal')}. -Nearby targets: {json.dumps(targets_summary)} +{map_section} +All known bots/parties on the board (sorted nearest first): {json.dumps(targets_summary)} Your ONLY legal moves this turn, with resulting coordinates and any strength penalty for squeezing past obstacles: {json.dumps(moves_summary)} Choose the direction that best serves your strategy (e.g. approach weaker solo bots to grow -your party, avoid stronger hostile parties, minimize strength penalties, or explore if nothing -is nearby). You MUST pick a key from the legal moves object above. +your party, avoid stronger hostile parties, route around obstacles visible on the map, minimize +strength penalties, or explore if nothing is nearby). You MUST pick a key from the legal moves +object above. Respond ONLY with JSON: {{"direction": "", "reasoning": "short reason"}} """