some backend rules for seeking wizard when low on health, tests, updated models in GEAR
This commit is contained in:
parent
5ed7bb927e
commit
0bc1c972b3
|
|
@ -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,28 +1959,34 @@ 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 = []
|
||||||
for other in self.players.values():
|
target_bot = None
|
||||||
if other.id == player.id or not other.is_alive or other.health <= 0:
|
if not seeking_wizard:
|
||||||
continue
|
for other in self.players.values():
|
||||||
if player.party_id and player.party_id == other.party_id:
|
if other.id == player.id or not other.is_alive or other.health <= 0:
|
||||||
continue
|
continue
|
||||||
dist = max(abs(player.x - other.x), abs(player.y - other.y))
|
if player.party_id and player.party_id == other.party_id:
|
||||||
targets.append((dist, other))
|
continue
|
||||||
|
dist = max(abs(player.x - other.x), abs(player.y - other.y))
|
||||||
|
targets.append((dist, other))
|
||||||
|
|
||||||
targets.sort(key=lambda t: t[0])
|
targets.sort(key=lambda t: t[0])
|
||||||
|
|
||||||
if bot_goal == "form_party":
|
if bot_goal == "form_party":
|
||||||
preferred = [t for t in targets if not t[1].party_id]
|
preferred = [t for t in targets if not t[1].party_id]
|
||||||
target_bot = preferred[0][1] if preferred else (targets[0][1] if targets else None)
|
target_bot = preferred[0][1] if preferred else (targets[0][1] if targets else None)
|
||||||
else:
|
else:
|
||||||
preferred = [t for t in targets if t[1].party_id]
|
preferred = [t for t in targets if t[1].party_id]
|
||||||
target_bot = preferred[0][1] if preferred else (targets[0][1] if targets else None)
|
target_bot = preferred[0][1] if preferred else (targets[0][1] if targets else None)
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"}}
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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!
|
||||||
|
|
|
||||||
|
|
@ -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"}}
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue