some backend rules for seeking wizard when low on health, tests, updated models in GEAR

This commit is contained in:
Isaac Johnson 2026-09-11 19:00:09 -05:00
parent 5ed7bb927e
commit 0bc1c972b3
8 changed files with 204 additions and 57 deletions

View File

@ -607,10 +607,42 @@ class GameEngine:
nearest = primary_targets[0] if primary_targets else (targets[0] if targets else None) nearest = primary_targets[0] if primary_targets else (targets[0] if targets else None)
wiz_dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y))
wiz_radar = WizardRadarTarget(
id=self.wizard.id,
name=self.wizard.name,
x=self.wizard.x,
y=self.wizard.y,
distance=wiz_dist,
strength=self.wizard.strength,
can_challenge=(wiz_dist <= 1),
)
rec_dir = None rec_dir = None
rec_act = "explore_unvisited" rec_act = "explore_unvisited"
if nearest: if player.health < 2 and self.wizard:
rec_act = "seek_wizard"
# Use BFS pathfinder to recommend direction navigating toward Gary the Wizard
bfs_path = self._find_path_bfs((player.x, player.y), (self.wizard.x, self.wizard.y))
if bfs_path and len(bfs_path) >= 2:
step_x, step_y = bfs_path[1]
dx = step_x - player.x
dy = step_y - player.y
else:
dx = 1 if self.wizard.x > player.x else (-1 if self.wizard.x < player.x else 0)
dy = 1 if self.wizard.y > player.y else (-1 if self.wizard.y < player.y else 0)
for name, (ox, oy) in DIRECTION_OFFSETS.items():
if ox == dx and oy == dy and "_" in name:
rec_dir = name
break
if not rec_dir:
for name, (ox, oy) in DIRECTION_OFFSETS.items():
if ox == dx and oy == dy:
rec_dir = name
break
elif nearest:
# Use BFS pathfinder to recommend direction navigating around obstacles! # Use BFS pathfinder to recommend direction navigating around obstacles!
bfs_path = self._find_path_bfs((player.x, player.y), (nearest.x, nearest.y)) bfs_path = self._find_path_bfs((player.x, player.y), (nearest.x, nearest.y))
if bfs_path and len(bfs_path) >= 2: if bfs_path and len(bfs_path) >= 2:
@ -642,17 +674,6 @@ class GameEngine:
else: else:
rec_act = "hunt_party" rec_act = "hunt_party"
wiz_dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y))
wiz_radar = WizardRadarTarget(
id=self.wizard.id,
name=self.wizard.name,
x=self.wizard.x,
y=self.wizard.y,
distance=wiz_dist,
strength=self.wizard.strength,
can_challenge=(wiz_dist <= 1),
)
return BotRadarResponse( return BotRadarResponse(
player_id=player.id, player_id=player.id,
current_x=player.x, current_x=player.x,
@ -1844,7 +1865,12 @@ class GameEngine:
f"It is not your turn. Current turn belongs to '{curr_name}' ({curr_id})." f"It is not your turn. Current turn belongs to '{curr_name}' ({curr_id})."
) )
bot_goal = "find_and_defeat_all_parties" if player.party_id else "form_party" seeking_wizard = bool(player.health < 2 and self.wizard)
bot_goal = (
"seek_wizard"
if seeking_wizard
else ("find_and_defeat_all_parties" if player.party_id else "form_party")
)
# 1. Check if already adjacent to encounter before moving # 1. Check if already adjacent to encounter before moving
formed_party, battle_res = self._check_adjacent_encounter(player) formed_party, battle_res = self._check_adjacent_encounter(player)
@ -1887,7 +1913,7 @@ class GameEngine:
effective_str = player.strength effective_str = player.strength
if player.party_id and player.party_id in self.parties: if player.party_id and player.party_id in self.parties:
effective_str = self.parties[player.party_id].total_strength effective_str = self.parties[player.party_id].total_strength
if effective_str >= self.wizard.strength or player.health >= 4: if effective_str >= self.wizard.strength or player.health >= 4 or player.health < 2:
if player.health <= 5: if player.health <= 5:
bot_reward = "health" bot_reward = "health"
elif player.strength < 4.0: elif player.strength < 4.0:
@ -1933,8 +1959,10 @@ class GameEngine:
turn=self._get_turn_info(), turn=self._get_turn_info(),
) )
# Find nearest target according to goal # Find nearest target according to goal or seek wizard if low health (< 2 HP)
targets = [] targets = []
target_bot = None
if not seeking_wizard:
for other in self.players.values(): for other in self.players.values():
if other.id == player.id or not other.is_alive or other.health <= 0: if other.id == player.id or not other.is_alive or other.health <= 0:
continue continue
@ -1954,7 +1982,11 @@ class GameEngine:
# Check if BFS pathfinder finds optimal route around obstacles # Check if BFS pathfinder finds optimal route around obstacles
bfs_next_step: Optional[Tuple[int, int]] = None bfs_next_step: Optional[Tuple[int, int]] = None
if target_bot: if seeking_wizard:
path = self._find_path_bfs((player.x, player.y), (self.wizard.x, self.wizard.y))
if path and len(path) >= 2:
bfs_next_step = (path[1][0] - player.x, path[1][1] - player.y)
elif target_bot:
path = self._find_path_bfs((player.x, player.y), (target_bot.x, target_bot.y)) path = self._find_path_bfs((player.x, player.y), (target_bot.x, target_bot.y))
if path and len(path) >= 2: if path and len(path) >= 2:
bfs_next_step = (path[1][0] - player.x, path[1][1] - player.y) bfs_next_step = (path[1][0] - player.x, path[1][1] - player.y)
@ -1973,7 +2005,11 @@ class GameEngine:
if bfs_next_step and (dx, dy) == bfs_next_step: if bfs_next_step and (dx, dy) == bfs_next_step:
score += 50.0 score += 50.0
if target_bot: if seeking_wizard:
old_dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y))
new_dist = max(abs(tx - self.wizard.x), abs(ty - self.wizard.y))
score += (old_dist - new_dist) * 10.0
elif target_bot:
old_dist = max(abs(player.x - target_bot.x), abs(player.y - target_bot.y)) old_dist = max(abs(player.x - target_bot.x), abs(player.y - target_bot.y))
new_dist = max(abs(tx - target_bot.x), abs(ty - target_bot.y)) new_dist = max(abs(tx - target_bot.x), abs(ty - target_bot.y))
score += (old_dist - new_dist) * 10.0 score += (old_dist - new_dist) * 10.0

View File

@ -364,7 +364,7 @@ class BotRadarResponse(BaseModel):
nearest_target: Optional[RadarTarget] = None nearest_target: Optional[RadarTarget] = None
wizard: Optional[WizardRadarTarget] = None wizard: Optional[WizardRadarTarget] = None
recommended_direction: Optional[str] = None recommended_direction: Optional[str] = None
recommended_action: str # "seek_partner", "form_party", "hunt_party", "engage_battle", "explore_unvisited", "challenge_wizard" recommended_action: str # "seek_partner", "form_party", "hunt_party", "engage_battle", "explore_unvisited", "challenge_wizard", "seek_wizard"
class BoardConfig(BaseModel): class BoardConfig(BaseModel):

View File

@ -765,3 +765,92 @@ def test_player_death_rule_and_scoreboard_preservation():
assert b3["id"] in ranking_ids assert b3["id"] in ranking_ids
def test_radar_seek_wizard_when_health_less_than_2():
"""Test that when a player's health is less than 2, get_bot_radar recommends 'seek_wizard'
and directs the player toward Gary the Wizard NPC."""
client = TestClient(app)
client.post("/api/reset")
# Register two players
p1 = client.post("/api/players", json={"name": "HurtBot", "color": "#ef4444", "strength": 3}).json()
p2 = client.post("/api/players", json={"name": "HealthyBot", "color": "#10b981", "strength": 3}).json()
# 1. Initially both players have 10 HP (>= 2), so recommended_action should NOT be seek_wizard
radar1 = client.get(f"/api/players/{p1['id']}/radar").json()
assert radar1["recommended_action"] != "seek_wizard"
assert radar1["recommended_action"] in ("seek_partner", "form_party", "explore_unvisited")
# 2. Set p1 health to 1 (< 2)
async def set_p1_low_health():
b1 = await game_engine.get_player(p1["id"])
b1.health = 1
asyncio.run(set_p1_low_health())
# Verify radar recommended_action is now 'seek_wizard'
radar_low = client.get(f"/api/players/{p1['id']}/radar").json()
assert radar_low["recommended_action"] == "seek_wizard"
assert radar_low["recommended_direction"] is not None
assert "wizard" in radar_low
assert radar_low["wizard"]["name"] == "Gary the Wizard"
# 3. Set p1 health to 2 (>= 2)
async def set_p1_health_2():
b1 = await game_engine.get_player(p1["id"])
b1.health = 2
asyncio.run(set_p1_health_2())
# Verify radar recommended_action reverts back from 'seek_wizard'
radar_hp2 = client.get(f"/api/players/{p1['id']}/radar").json()
assert radar_hp2["recommended_action"] != "seek_wizard"
def test_step_bot_ai_seeks_and_challenges_wizard_when_low_health():
"""Test that step_bot_ai navigates towards the wizard when HP < 2,
and voluntarily challenges the wizard for healing upon arriving adjacent."""
client = TestClient(app)
client.post("/api/reset")
# Register a weak bot with 1 HP
p = client.post("/api/players", json={"name": "SickBot", "color": "#10b981", "strength": 1}).json()
wiz = client.get("/api/wizard").json()
wx, wy = wiz["x"], wiz["y"]
# Place bot 2 steps away from Gary the Wizard (ensure within bounds)
bot_x = wx + 2 if wx <= 62 else wx - 2
bot_y = wy
async def setup_low_hp_bot():
b = await game_engine.get_player(p["id"])
b.x = bot_x
b.y = bot_y
b.health = 1
b.strength = 1.0 # Weaker than Gary (3.0)
asyncio.run(setup_low_hp_bot())
client.post("/api/game/start")
game_engine.turn_order = [p["id"]]
game_engine.current_turn_index = 0
# 1. First ai-step: Should navigate closer to the wizard with bot_goal='seek_wizard'
step1_res = client.post(f"/api/players/{p['id']}/ai-step")
assert step1_res.status_code == 200
step1_data = step1_res.json()
assert step1_data["bot_goal"] == "seek_wizard"
assert step1_data["action_taken"] == "moved"
# Distance to wizard should have decreased
dist_after_move = max(abs(step1_data["move_result"]["new_position"]["x"] - wx),
abs(step1_data["move_result"]["new_position"]["y"] - wy))
assert dist_after_move <= 1
# 2. Place bot adjacent to Gary and execute ai-step: should challenge Gary for health reward!
game_engine.turn_order = [p["id"]]
game_engine.current_turn_index = 0
step2_res = client.post(f"/api/players/{p['id']}/ai-step")
assert step2_res.status_code == 200
step2_data = step2_res.json()
assert step2_data["action_taken"] == "challenged_wizard"
assert step2_data["wizard_challenge_result"] is not None

View File

@ -132,8 +132,8 @@ class SmartBotAgent:
if wizard and wizard.get("can_challenge"): if wizard and wizard.get("can_challenge"):
my_health = my_info.get("health", 10) my_health = my_info.get("health", 10)
wiz_str = wizard.get("strength", 3.0) wiz_str = wizard.get("strength", 3.0)
# Challenge wizard if bot has >= wizard strength or healthy enough (HP >= 4) # Challenge wizard if bot has >= wizard strength, healthy enough (HP >= 4), or low health seeking healing (HP < 2)
if self.strength >= wiz_str or my_health >= 4: if self.strength >= wiz_str or my_health >= 4 or my_health < 2:
print(f"🧙 [WIZARD NEARBY] Adjacent to {wizard.get('name', 'Gary the Wizard')} (Str: {wiz_str})! HP: {my_health}, Bot Str: {self.strength}. Choosing to challenge!") print(f"🧙 [WIZARD NEARBY] Adjacent to {wizard.get('name', 'Gary the Wizard')} (Str: {wiz_str})! HP: {my_health}, Bot Str: {self.strength}. Choosing to challenge!")
self._challenge_wizard(my_health=my_health) self._challenge_wizard(my_health=my_health)
return return

View File

@ -44,7 +44,7 @@ Rules you must respect when choosing among the OPTIONS given to you:
- Diagonal squeezes between obstacle corners cost strength (0.1 solo, 0.2 leader / 0.1 followers). - Diagonal squeezes between obstacle corners cost strength (0.1 solo, 0.2 leader / 0.1 followers).
- The Wandering Wizard NPC can be voluntarily challenged when adjacent (distance <= 1). The challenge - The Wandering Wizard NPC can be voluntarily challenged when adjacent (distance <= 1). The challenge
is a 3-bout D20 duel (strength * D20). If victorious, the player decides whether to receive +2 score, is a 3-bout D20 duel (strength * D20). If victorious, the player decides whether to receive +2 score,
+2 strength, or +2 health; losing costs 2 health (or score if no health). +2 strength, or +2 health; losing costs 2 health (or score if no health). Challenging the wizard is the only way to add health in the game.
- The game ends when all surviving bots are united into a single remaining party. - The game ends when all surviving bots are united into a single remaining party.
You will only ever be asked to choose between options that are legal - always answer with the You will only ever be asked to choose between options that are legal - always answer with the
requested JSON object and nothing else. requested JSON object and nothing else.
@ -529,8 +529,8 @@ squeezing past obstacles: {json.dumps(moves_summary)}
Choose the direction that best serves your strategy (e.g. approach weaker solo bots to grow Choose the direction that best serves your strategy (e.g. approach weaker solo bots to grow
your party, avoid stronger hostile parties, route around obstacles visible on the map, minimize 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 strength penalties, go to the Gary the Wizard NPC for points or health). As you are a risk taker, the Wizard is an ideal target
object above. You MUST pick a key from the legal moves object above.
Respond ONLY with JSON: {{"direction": "<one of {available}>", "reasoning": "short reason"}} Respond ONLY with JSON: {{"direction": "<one of {available}>", "reasoning": "short reason"}}
""" """

View File

@ -56,15 +56,15 @@ python3 bot.py \
--name "GeminiTitan" \ --name "GeminiTitan" \
--color "#0ea5e9" \ --color "#0ea5e9" \
--strength 6 \ --strength 6 \
--model "gemini-2.5-flash" \ --model "gemini-3.8-flash" \
--project "YOUR_GCP_PROJECT_ID" --project "YOUR_GCP_PROJECT_ID"
``` ```
#### Using Environment Variables: #### Using Environment Variables:
```bash ```bash
export VERTEX_PROJECT_ID="YOUR_GCP_PROJECT_ID" export VERTEX_PROJECT_ID="YOUR_GCP_PROJECT_ID"
export VERTEX_LOCATION="us-central1" export VERTEX_LOCATION="global"
export VERTEX_MODEL="gemini-2.5-flash" export VERTEX_MODEL="gemini-3.8-flash"
export BOT_NAME="GearBot" export BOT_NAME="GearBot"
export BOT_COLOR="#10b981" export BOT_COLOR="#10b981"
export BOT_STRENGTH="5" export BOT_STRENGTH="5"
@ -89,8 +89,8 @@ python3 bot.py --url "http://192.168.1.100:8000/api" --name "RemoteGear"
| `-s` | `--strength` | `BOT_STRENGTH` | `5` | Starting strength (1 to 10) | | `-s` | `--strength` | `BOT_STRENGTH` | `5` | Starting strength (1 to 10) |
| `-H` | `--health` | `BOT_HEALTH` | `10` | Starting health points (default 10) | | `-H` | `--health` | `BOT_HEALTH` | `10` | Starting health points (default 10) |
| `-p` | `--project` | `VERTEX_PROJECT_ID` | Auto-detected from `gcloud` | Google Cloud Project ID | | `-p` | `--project` | `VERTEX_PROJECT_ID` | Auto-detected from `gcloud` | Google Cloud Project ID |
| `-l` | `--location` | `VERTEX_LOCATION` | `us-central1` | Google Cloud region for Vertex AI | | `-l` | `--location` | `VERTEX_LOCATION` | `global` | Google Cloud region for Vertex AI (e.g. `global`, `us-central1`) |
| `-m` | `--model` | `VERTEX_MODEL` | `gemini-2.5-flash` | Gemini model name | | `-m` | `--model` | `VERTEX_MODEL` | `gemini-3.8-flash` | Gemini model name |
| `-k` | `--api-key` | `VERTEX_API_KEY` / `GEMINI_API_KEY` | *(None)* | Optional Gemini API key | | `-k` | `--api-key` | `VERTEX_API_KEY` / `GEMINI_API_KEY` | *(None)* | Optional Gemini API key |
--- ---

View File

@ -70,8 +70,8 @@ Follow the browser prompt to grant access.
Add to your `~/.bashrc` or run in your terminal: Add to your `~/.bashrc` or run in your terminal:
```bash ```bash
export VERTEX_PROJECT_ID=$(gcloud config get-value project) export VERTEX_PROJECT_ID=$(gcloud config get-value project)
export VERTEX_LOCATION="us-central1" export VERTEX_LOCATION="global"
export VERTEX_MODEL="gemini-2.5-flash" export VERTEX_MODEL="gemini-3.8-flash"
``` ```
--- ---
@ -136,10 +136,11 @@ import subprocess, requests, json, os
token = os.getenv('VERTEX_ACCESS_TOKEN') or subprocess.check_output(['gcloud', 'auth', 'print-access-token'], text=True).strip() token = os.getenv('VERTEX_ACCESS_TOKEN') or subprocess.check_output(['gcloud', 'auth', 'print-access-token'], text=True).strip()
project = os.getenv('VERTEX_PROJECT_ID') or subprocess.check_output(['gcloud', 'config', 'get-value', 'project'], text=True).strip() project = os.getenv('VERTEX_PROJECT_ID') or subprocess.check_output(['gcloud', 'config', 'get-value', 'project'], text=True).strip()
location = os.getenv('VERTEX_LOCATION', 'us-central1') location = os.getenv('VERTEX_LOCATION', 'global')
model = os.getenv('VERTEX_MODEL', 'gemini-2.5-flash') model = os.getenv('VERTEX_MODEL', 'gemini-3.8-flash')
url = f'https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent' host = 'aiplatform.googleapis.com' if location == 'global' else f'{location}-aiplatform.googleapis.com'
url = f'https://{host}/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent'
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
payload = {'contents': [{'role': 'user', 'parts': [{'text': 'Hello Gemini'}]}]} payload = {'contents': [{'role': 'user', 'parts': [{'text': 'Hello Gemini'}]}]}
@ -158,7 +159,7 @@ else:
|---|---|---| |---|---|---|
| `403 PermissionDenied: Vertex AI API has not been used...` | API is disabled | Run `gcloud services enable aiplatform.googleapis.com` | | `403 PermissionDenied: Vertex AI API has not been used...` | API is disabled | Run `gcloud services enable aiplatform.googleapis.com` |
| `401 Unauthorized` / `Token expired` | Token expired or invalid | Re-run `gcloud auth application-default login` or refresh `gcloud auth login` | | `401 Unauthorized` / `Token expired` | Token expired or invalid | Re-run `gcloud auth application-default login` or refresh `gcloud auth login` |
| `404 Publisher model ... not found` | Region does not have the model | Default to `us-central1`, `us-east4`, or check model name (`gemini-2.5-flash`, `gemini-1.5-flash`) | | `404 Publisher model ... not found` | Region does not have the model | Modern Gemini 3.x models (such as `gemini-3.8-flash`) are deployed in `global` (`aiplatform.googleapis.com`), while older models may be in `us-central1`. `bot.py` handles auto-fallback between `global` and `us-central1`. |
| `No Google Cloud project ID detected` | Project is not set | Run `gcloud config set project <PROJECT_ID>` or export `VERTEX_PROJECT_ID` | | `No Google Cloud project ID detected` | Project is not set | Run `gcloud config set project <PROJECT_ID>` or export `VERTEX_PROJECT_ID` |
Once setup is complete, proceed to [INSTALL.md](INSTALL.md) and [README.md](README.md) to install dependencies and run your agent! Once setup is complete, proceed to [INSTALL.md](INSTALL.md) and [README.md](README.md) to install dependencies and run your agent!

View File

@ -38,7 +38,7 @@ DEFAULT_BOT_COLOR = "#4285f4"
DEFAULT_BOT_STRENGTH = 5 DEFAULT_BOT_STRENGTH = 5
DEFAULT_BOT_HEALTH = 10 DEFAULT_BOT_HEALTH = 10
DEFAULT_MODEL = "gemini-3.8-flash" DEFAULT_MODEL = "gemini-3.8-flash"
DEFAULT_LOCATION = "us-central1" DEFAULT_LOCATION = "global"
GAME_RULES_SUMMARY = """ GAME_RULES_SUMMARY = """
Rules you must respect when choosing among the OPTIONS given to you: Rules you must respect when choosing among the OPTIONS given to you:
@ -54,7 +54,7 @@ Rules you must respect when choosing among the OPTIONS given to you:
- Diagonal squeezes between obstacle corners cost strength (0.1 solo, 0.2 leader / 0.1 followers). - Diagonal squeezes between obstacle corners cost strength (0.1 solo, 0.2 leader / 0.1 followers).
- The Wandering Wizard NPC can be voluntarily challenged when adjacent (distance <= 1). The challenge - The Wandering Wizard NPC can be voluntarily challenged when adjacent (distance <= 1). The challenge
is a 3-bout D20 duel (strength * D20). If victorious, the player decides whether to receive +2 score, is a 3-bout D20 duel (strength * D20). If victorious, the player decides whether to receive +2 score,
+2 strength, or +2 health; losing costs 2 health (or score if no health). +2 strength, or +2 health; losing costs 2 health (or score if no health). Challenging the wizard is the only way to add health in the game.
- The game ends when all surviving bots are united into a single remaining party. - The game ends when all surviving bots are united into a single remaining party.
You will only ever be asked to choose between options that are legal - always answer with the You will only ever be asked to choose between options that are legal - always answer with the
requested JSON object and nothing else. requested JSON object and nothing else.
@ -204,6 +204,19 @@ class VertexGeminiClient:
return True, f"Authenticated to GCP Project '{self.project_id}' in region '{self.location}' (model: {self.model})" return True, f"Authenticated to GCP Project '{self.project_id}' in region '{self.location}' (model: {self.model})"
def _build_vertex_url(self, location: Optional[str] = None) -> str:
"""Construct the Vertex AI REST generateContent URL."""
loc = location or self.location or "global"
if loc == "global":
host = "aiplatform.googleapis.com"
else:
host = f"{loc}-aiplatform.googleapis.com"
return (
f"https://{host}/v1/"
f"projects/{self.project_id}/locations/{loc}/"
f"publishers/google/models/{self.model}:generateContent"
)
def ask_json(self, prompt: str) -> Optional[Dict[str, Any]]: def ask_json(self, prompt: str) -> Optional[Dict[str, Any]]:
"""Query Gemini requesting structured JSON output.""" """Query Gemini requesting structured JSON output."""
payload = { payload = {
@ -232,11 +245,7 @@ class VertexGeminiClient:
print("⚠️ [VERTEX AUTH ERROR] Not authenticated. See botagent_gear/SETUP.md.") print("⚠️ [VERTEX AUTH ERROR] Not authenticated. See botagent_gear/SETUP.md.")
return None return None
url = ( url = self._build_vertex_url()
f"https://{self.location}-aiplatform.googleapis.com/v1/"
f"projects/{self.project_id}/locations/{self.location}/"
f"publishers/google/models/{self.model}:generateContent"
)
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
if token: if token:
headers["Authorization"] = f"Bearer {token}" headers["Authorization"] = f"Bearer {token}"
@ -253,6 +262,18 @@ class VertexGeminiClient:
headers["Authorization"] = f"Bearer {new_token}" headers["Authorization"] = f"Bearer {new_token}"
res = requests.post(url, headers=headers, json=payload, timeout=45) res = requests.post(url, headers=headers, json=payload, timeout=45)
# Auto-fallback location on 404 (e.g. if model is hosted in 'global' vs regional 'us-central1')
if res.status_code == 404 and self.project_id:
fallback_loc = "global" if self.location != "global" else "us-central1"
fallback_url = self._build_vertex_url(fallback_loc)
if self.api_key:
fallback_url = f"{fallback_url}?key={self.api_key}"
res_fb = requests.post(fallback_url, headers=headers, json=payload, timeout=45)
if res_fb.status_code == 200:
print(f" [VERTEX] Model '{self.model}' located in '{fallback_loc}' (switched from '{self.location}').")
self.location = fallback_loc
res = res_fb
res.raise_for_status() res.raise_for_status()
data = res.json() data = res.json()
@ -715,8 +736,8 @@ squeezing past obstacles: {json.dumps(moves_summary)}
Choose the direction that best serves your strategy (e.g. approach weaker solo bots to grow Choose the direction that best serves your strategy (e.g. approach weaker solo bots to grow
your party, avoid stronger hostile parties, route around obstacles visible on the map, minimize 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 strength penalties, go to the Gary the Wizard NPC for points or health). As you are a risk taker, the Wizard is an ideal target
object above. You MUST pick a key from the legal moves object above.
Respond ONLY with JSON: {{"direction": "<one of {available}>", "reasoning": "short reason"}} Respond ONLY with JSON: {{"direction": "<one of {available}>", "reasoning": "short reason"}}
""" """