From 5ed7bb927e31d9db8b5d0299046d0b949aa5a7b4 Mon Sep 17 00:00:00 2001 From: Isaac Johnson Date: Wed, 9 Sep 2026 19:46:30 -0500 Subject: [PATCH] Gary the Wizard --- AGENTS.md | 26 +++++++++---------- GAME_RULES.md | 12 ++++----- backend/app/game.py | 9 ++++--- backend/app/models.py | 6 ++--- backend/tests/test_api.py | 2 +- botagent/bot_agent.py | 4 +-- botagent_ai/bot.py | 6 ++--- botagent_gear/bot.py | 6 ++--- frontend/src/components/BoardCanvas.tsx | 2 +- .../src/components/WizardChallengeModal.tsx | 2 +- frontend/src/components/WizardPromptModal.tsx | 4 +-- frontend/src/hooks/useGameSocket.ts | 4 +-- frontend/src/utils/pixelAvatars.tsx | 2 +- 13 files changed, 43 insertions(+), 42 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 91aadcb..423b5fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,12 +110,12 @@ When writing bots, backend engine logic, or game simulations, adhere strictly to - **Party**: Leader loses -0.2 strength; followers each lose -0.1 strength. - Moving along outer obstacle edges has no penalty. Minimum bot strength floor is 0.1. -### 6. The Wizard (NPC Encounter & Challenge) -- A wandering Wizard NPC roams the map at a random passable coordinate. -- Players (or party leaders) who locate the wizard (adjacent or on same tile, distance <= 1) may voluntarily **choose to challenge** the wizard. +### 6. Gary the Wizard (NPC Encounter & Challenge) +- Gary the Wizard is a wandering NPC who roams the map at a random passable coordinate. +- Players (or party leaders) who locate Gary the Wizard (adjacent or on same tile, distance <= 1) may voluntarily **choose to challenge** him. - **Challenge Resolution (3-Bout D20)**: - Exactly 3 bouts are conducted. - - In each bout, each side rolls a D20 die, multiplied by their strength (the wizard has 3.0 strength; parties use squad total strength). + - In each bout, each side rolls a D20 die, multiplied by their strength (Gary the Wizard has 3.0 strength; parties use squad total strength). - Highest bout score wins the bout. Best 2-out-of-3 bouts wins the challenge. - **Victory Rewards & Defeat Penalties**: - **Victory**: The player (or party leader) decides and chooses which reward to claim: @@ -124,11 +124,11 @@ When writing bots, backend engine logic, or game simulations, adhere strictly to - **+2 Health**: Adds +2 health points (expanding max health if current health exceeds initial maximum). - **Defeat**: Player/party leader loses **2 health** (or **2 score points** if they do not have health to lose). - **Post-Challenge**: - - Following the challenge, the wizard teleports to a new random open coordinate on the map. + - Following the challenge, Gary the Wizard teleports to a new random open coordinate on the map. ### 7. Player Health, Damage & Death - All bots register with default **10 HP** (configurable). -- Health damage is sustained by losing battles (-1 to -3 HP) or losing Wizard challenges (-2 HP). +- Health damage is sustained by losing battles (-1 to -3 HP) or losing Gary the Wizard challenges (-2 HP). - When a bot's health drops to **0 HP**, they are **dead**. - **Death Consequences**: - **Party Disconnection**: The dead bot is immediately detached from any party. If the dead bot was the leader, squad leadership transfers to the strongest surviving squad member (or the party dissolves if empty). Defeated dead followers are not absorbed. @@ -154,9 +154,9 @@ The backend serves both REST endpoints under `/api` and a live WebSocket stream | `GET` | `/api/players` | List all active players, scores, health, and positions | | `GET` | `/api/board` | Full board state (grid, obstacles, players, parties, wizard, current turn) | | `POST` | `/api/board/reset` | Clear board, reset parties, reset players, and respawn wizard | -| `GET` | `/api/wizard` | Get current Wizard NPC coordinates and attributes | -| `POST` | `/api/wizard/challenge` | Challenge the Wizard NPC: `{"player_id": str, "reward_choice": "score"|"strength"|"health"}` (3-bout D20 duel) | -| `GET` | `/api/players/{id}/radar` | Scans surroundings, finds closest bots and Wizard NPC | +| `GET` | `/api/wizard` | Get current Gary the Wizard NPC coordinates and attributes | +| `POST` | `/api/wizard/challenge` | Challenge Gary the Wizard NPC: `{"player_id": str, "reward_choice": "score"|"strength"|"health"}` (3-bout D20 duel) | +| `GET` | `/api/players/{id}/radar` | Scans surroundings, finds closest bots and Gary the Wizard NPC | | `GET` | `/api/players/{id}/memory` | Coordinate history and visited locations | | `GET` | `/api/players/{id}/available-moves` | Valid movements in all 8 directions (evaluates terrain & obstacles) | | `POST` | `/api/players/{id}/move` | Execute a move (`{"direction": "N"|"S"|"E"|"W"|"NE"|"NW"|"SE"|"SW"}`) | @@ -211,11 +211,11 @@ The application is containerized into a single unified image via [Dockerfile](Do - **Workflow**: 1. Registers bot via `POST /api/players`. 2. Polls `GET /api/turn` to wait for its turn. - 3. Uses `GET /api/players/{id}/radar` to find nearest target and detect Wizard NPC proximity. + 3. Uses `GET /api/players/{id}/radar` to find nearest target and detect Gary the Wizard NPC proximity. 4. Avoids looping using internal coordinate history. 5. Computes vector direction, evaluates diagonal obstacles, and moves. 6. Evaluates alliances vs. fights strictly according to strength hierarchy. - 7. Decides whether to challenge the Wizard NPC based on relative strength and health advantage. + 7. Decides whether to challenge Gary the Wizard NPC based on relative strength and health advantage. - **Run Command**: ```bash python3 botagent/bot_agent.py --name CyberBot --color "#10b981" -s 4 -H 10 --url http://localhost:8000/api @@ -228,7 +228,7 @@ The application is containerized into a single unified image via [Dockerfile](Do - Communicates with an LLM backend (configured for local/remote Ollama HTTP API at `/api/generate` with model `gemma4:12b`, or adaptable to OpenAI-compatible endpoints). - Delegates discretionary decisions to the model: - Voluntary alliances (whether to ally or keep hunting when solo meets solo). - - Voluntary Wizard challenges (whether to challenge the Wizard NPC when adjacent for +2 score vs. -2 health risk). + - Voluntary Wizard challenges (whether to challenge Gary the Wizard NPC when adjacent for +2 score vs. -2 health risk). - Navigation direction toward radar targets while balancing obstacle squeeze trade-offs. - Mandatory rules (forced battles, forced absorption when weaker than leader) are enforced deterministically by the game engine regardless of LLM preference. - **Configuration & Environment Variables**: @@ -252,7 +252,7 @@ The application is containerized into a single unified image via [Dockerfile](Do - **LLM Integration**: - Communicates with Google Cloud Vertex AI generateContent REST endpoint or Google AI Studio Gemini API. - Features structured JSON generation (`responseMimeType: application/json`) and system instruction enforcement. - - Delegates discretionary choices to Gemini: voluntary alliances, choosing whether to challenge the Wizard NPC, and spatial pathing. + - Delegates discretionary choices to Gemini: voluntary alliances, choosing whether to challenge Gary the Wizard NPC, and spatial pathing. - Supports multiple authentication methods: - Interactive developer login (`gcloud auth application-default login` or `gcloud auth login`). - Automated service account keys (`GOOGLE_APPLICATION_CREDENTIALS`). diff --git a/GAME_RULES.md b/GAME_RULES.md index 41b6f42..14b4ff3 100644 --- a/GAME_RULES.md +++ b/GAME_RULES.md @@ -28,11 +28,11 @@ - In a party, the leader loses **0.2 strength**, and follower members each lose **0.1 strength**. 4. Moving along the open outer edge of an obstacle incurs no penalty. Minimum bot strength cannot fall below 0.1. -6. **The Wizard (NPC Encounter & Challenge)**: - 1. The Wizard is a special Non-Player Character (NPC) roaming the map at a random passable location. - 2. Players (or party leaders) who find the wizard (adjacent or on the same coordinate, distance <= 1) may voluntarily **choose to challenge** the wizard. +6. **Gary the Wizard (NPC Encounter & Challenge)**: + 1. Gary the Wizard is a special Non-Player Character (NPC) roaming the map at a random passable location. + 2. Players (or party leaders) who find Gary the Wizard (adjacent or on the same coordinate, distance <= 1) may voluntarily **choose to challenge** him. 3. **Challenge Resolution (3-Bout D20)**: - - Challenges consist of 3 bouts: each side rolls a D20 die, multiplied by their strength (the wizard has a strength of 3.0; a party uses its squad total strength). + - Challenges consist of 3 bouts: each side rolls a D20 die, multiplied by their strength (Gary the Wizard has a strength of 3.0; a party uses its squad total strength). - Highest bout score wins the bout. Best 2 out of 3 bouts wins the challenge. 4. **Victory Rewards & Defeat Penalties**: - **Victory**: The player (or party leader) must decide and choose which reward to receive: @@ -41,11 +41,11 @@ - **+2 Health**: Adds +2 health points (HP) to the bot (expanding max health if current health exceeds initial maximum). - **Defeat**: The player (or party leader) loses **2 health** (or **2 score points** if they do not have health to lose). 5. **Wizard Relocation**: - - Following any challenge, the wizard teleports to a new random open coordinate on the map. + - Following any challenge, Gary the Wizard teleports to a new random open coordinate on the map. 7. **Player Health, Damage & Death**: 1. All bots register with default **10 HP** (configurable via `--health` / `-H`). - 2. Health damage is sustained by losing battles (-1 to -3 HP) or losing Wizard challenges (-2 HP). + 2. Health damage is sustained by losing battles (-1 to -3 HP) or losing Gary the Wizard challenges (-2 HP). 3. When a player's health points reach **0 HP**, they are **dead**. 4. **Death Consequences**: - **Party Disconnection**: The dead player is immediately disconnected from any existing party. If the dead player was the party leader, squad leadership transfers to the strongest surviving member (or the party dissolves if no living members remain). Dead followers are not absorbed into opposing parties. diff --git a/backend/app/game.py b/backend/app/game.py index 71e65c2..47e1ef0 100644 --- a/backend/app/game.py +++ b/backend/app/game.py @@ -73,7 +73,7 @@ class GameEngine: wx, wy = self._find_random_free_position() return WizardNPC( id="wizard_npc", - name="Grand Wizard", + name="Gary the Wizard", x=wx, y=wy, strength=3.0, @@ -1486,7 +1486,7 @@ class GameEngine: return WizardChallengeResult( challenger_id=player.id, challenger_name=player.name, - wizard_name="Grand Wizard", + wizard_name=self.wizard.name if self.wizard else "Gary the Wizard", party_id=party_id, bouts=bouts, player_bouts_won=player_bouts_won, @@ -1532,8 +1532,9 @@ class GameEngine: dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y)) if dist > 1: + wiz_name = self.wizard.name if self.wizard else "Gary the Wizard" raise ValueError( - f"Player '{player.name}' is not adjacent to the Grand Wizard (distance {dist}). Must be within 1 distance." + f"Player '{player.name}' is not adjacent to {wiz_name} (distance {dist}). Must be within 1 distance." ) return self._resolve_wizard_challenge_internal(player, reward_choice=reward_choice) @@ -1880,7 +1881,7 @@ class GameEngine: turn=self._get_turn_info(), ) - # 1b. Check if adjacent to the Grand Wizard NPC and choose to challenge + # 1b. Check if adjacent to Gary the Wizard NPC and choose to challenge wiz_dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y)) if wiz_dist <= 1 and (not player.party_id or player.is_party_leader): effective_str = player.strength diff --git a/backend/app/models.py b/backend/app/models.py index b670569..bea4ede 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -268,7 +268,7 @@ class BattleRequest(BaseModel): class WizardNPC(BaseModel): id: str = "wizard_npc" - name: str = "Grand Wizard" + name: str = "Gary the Wizard" x: int y: int strength: float = 3.0 @@ -290,7 +290,7 @@ class WizardChallengeBout(BaseModel): class WizardChallengeResult(BaseModel): challenger_id: str challenger_name: str - wizard_name: str = "Grand Wizard" + wizard_name: str = "Gary the Wizard" party_id: Optional[str] = None bouts: List[WizardChallengeBout] player_bouts_won: int @@ -317,7 +317,7 @@ class WizardChallengeRequest(BaseModel): class WizardRadarTarget(BaseModel): id: str = "wizard_npc" - name: str = "Grand Wizard" + name: str = "Gary the Wizard" x: int y: int distance: int diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 96b1d22..6c1521f 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -484,7 +484,7 @@ def test_wizard_npc_existence_and_radar_detection(): assert wiz_res.status_code == 200 wiz_data = wiz_res.json() assert wiz_data["id"] == "wizard_npc" - assert wiz_data["name"] == "Grand Wizard" + assert wiz_data["name"] == "Gary the Wizard" assert wiz_data["strength"] == 3.0 assert 0 <= wiz_data["x"] <= 64 assert 0 <= wiz_data["y"] <= 64 diff --git a/botagent/bot_agent.py b/botagent/bot_agent.py index b897547..2df3c26 100644 --- a/botagent/bot_agent.py +++ b/botagent/bot_agent.py @@ -134,7 +134,7 @@ class SmartBotAgent: wiz_str = wizard.get("strength", 3.0) # Challenge wizard if bot has >= wizard strength or healthy enough (HP >= 4) if self.strength >= wiz_str or my_health >= 4: - print(f"🧙 [WIZARD NEARBY] Adjacent to {wizard.get('name', 'Grand 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) return @@ -250,7 +250,7 @@ class SmartBotAgent: else: reward_choice = "score" - print(f"🧙 [WIZARD CHALLENGE] Challenging Grand Wizard to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") + print(f"🧙 [WIZARD CHALLENGE] Challenging Gary the Wizard to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") try: res = requests.post( f"{self.base_url}/wizard/challenge", diff --git a/botagent_ai/bot.py b/botagent_ai/bot.py index 5ba4569..606983c 100644 --- a/botagent_ai/bot.py +++ b/botagent_ai/bot.py @@ -212,7 +212,7 @@ class AIBotAgent: prompt = f"""{GAME_RULES_SUMMARY} You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, health {my_info.get('health', 10)}) at position ({my_info['x']}, {my_info['y']}). -You are adjacent to the NPC Grand Wizard (strength {wizard['strength']}). +You are adjacent to the NPC Gary the Wizard (strength {wizard['strength']}). Challenging the wizard initiates a 3-bout D20 duel (strength * roll). - If you win: You choose one reward: +2 score, +2 strength, or +2 health! - If you lose: -2 health points (or -2 score if no health)! @@ -231,7 +231,7 @@ Respond ONLY with JSON: {{"challenge_wizard": true|false, "reward_choice": "scor def _challenge_wizard(self, wizard: Dict[str, Any], reward_choice: str = "score"): """Execute the challenge against the Wizard NPC.""" - print(f"🧙 [WIZARD CHALLENGE] Challenging {wizard.get('name', 'Grand Wizard')} to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") + print(f"🧙 [WIZARD CHALLENGE] Challenging {wizard.get('name', 'Gary the Wizard')} to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") try: res = requests.post( f"{self.base_url}/wizard/challenge", @@ -513,7 +513,7 @@ Respond ONLY with JSON: {{"form_alliance": true|false, "reasoning": "short reaso 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, -W = Grand Wizard NPC, M = mountain, F = forest, V = valley, # = out of bounds, . = open ground. +W = Gary the Wizard NPC, M = mountain, F = forest, V = valley, # = out of bounds, . = open ground. {chr(10).join(local_map)} """ diff --git a/botagent_gear/bot.py b/botagent_gear/bot.py index b306af2..c5c52d9 100644 --- a/botagent_gear/bot.py +++ b/botagent_gear/bot.py @@ -398,7 +398,7 @@ class VertexAIBotAgent: prompt = f"""{GAME_RULES_SUMMARY} You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, health {my_info.get('health', 10)}) at position ({my_info['x']}, {my_info['y']}). -You are adjacent to the NPC Grand Wizard (strength {wizard['strength']}). +You are adjacent to the NPC Gary the Wizard (strength {wizard['strength']}). Challenging the wizard initiates a 3-bout D20 duel (strength * roll). - If you win: You choose one reward: +2 score, +2 strength, or +2 health! - If you lose: -2 health points (or -2 score if no health)! @@ -417,7 +417,7 @@ Respond ONLY with JSON: {{"challenge_wizard": true|false, "reward_choice": "scor def _challenge_wizard(self, wizard: Dict[str, Any], reward_choice: str = "score"): """Execute the challenge against the Wizard NPC.""" - print(f"🧙 [WIZARD CHALLENGE] Challenging {wizard.get('name', 'Grand Wizard')} to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") + print(f"🧙 [WIZARD CHALLENGE] Challenging {wizard.get('name', 'Gary the Wizard')} to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") try: res = requests.post( f"{self.base_url}/wizard/challenge", @@ -699,7 +699,7 @@ Respond ONLY with JSON: {{"form_alliance": true|false, "reasoning": "short reaso 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, -W = Grand Wizard NPC, M = mountain, F = forest, V = valley, # = out of bounds, . = open ground. +W = Gary the Wizard NPC, M = mountain, F = forest, V = valley, # = out of bounds, . = open ground. {chr(10).join(local_map)} """ diff --git a/frontend/src/components/BoardCanvas.tsx b/frontend/src/components/BoardCanvas.tsx index dec44ca..c410601 100644 --- a/frontend/src/components/BoardCanvas.tsx +++ b/frontend/src/components/BoardCanvas.tsx @@ -623,7 +623,7 @@ export const BoardCanvas: React.FC = ({ | 🧙‍♂️ - Grand Wizard NPC (Str: {boardState.wizard?.strength}) + Gary the Wizard NPC (Str: {boardState.wizard?.strength}) )} diff --git a/frontend/src/components/WizardChallengeModal.tsx b/frontend/src/components/WizardChallengeModal.tsx index 8a9547a..897ea0d 100644 --- a/frontend/src/components/WizardChallengeModal.tsx +++ b/frontend/src/components/WizardChallengeModal.tsx @@ -60,7 +60,7 @@ export const WizardChallengeModal: React.FC = ({ if (!challenge) return null; const playerName = challenge.challenger_name; - const wizardName = challenge.wizard_name || 'Grand Wizard'; + const wizardName = challenge.wizard_name || 'Gary the Wizard'; return (
diff --git a/frontend/src/components/WizardPromptModal.tsx b/frontend/src/components/WizardPromptModal.tsx index f8d13bf..5a30cb5 100644 --- a/frontend/src/components/WizardPromptModal.tsx +++ b/frontend/src/components/WizardPromptModal.tsx @@ -20,7 +20,7 @@ export const WizardPromptModal: React.FC = ({ if (!isOpen || !challenger) return null; - const wizardName = wizard?.name || 'Grand Wizard'; + const wizardName = wizard?.name || 'Gary the Wizard'; const wizardStr = wizard?.strength ?? 3.0; const options: Array<{ @@ -73,7 +73,7 @@ export const WizardPromptModal: React.FC = ({ 🧙‍♂️

- CHALLENGE THE GRAND WIZARD + CHALLENGE GARY THE WIZARD

Select your victory reward before entering the 3-bout D20 duel diff --git a/frontend/src/hooks/useGameSocket.ts b/frontend/src/hooks/useGameSocket.ts index 3e12970..ab28c74 100644 --- a/frontend/src/hooks/useGameSocket.ts +++ b/frontend/src/hooks/useGameSocket.ts @@ -215,7 +215,7 @@ export function useGameSocket() { })); const c: WizardChallengeResult = data.challenge_result || data.challenge; setActiveWizardChallenge(c); - const wizName = c.wizard_name || 'Grand Wizard'; + const wizName = c.wizard_name || 'Gary the Wizard'; let rewardLabel = `+${c.score_change} score`; if (c.reward_chosen === 'strength') { rewardLabel = `+${c.strength_change ?? 2} strength`; @@ -495,7 +495,7 @@ export function useGameSocket() { } else if (data.action_taken === 'challenged_wizard' && data.wizard_challenge_result) { const wcr = data.wizard_challenge_result; setActiveWizardChallenge(wcr); - const wizName = wcr.wizard_name || 'Grand Wizard'; + const wizName = wcr.wizard_name || 'Gary the Wizard'; setLastEventMessage( wcr.player_won ? `🧙 ${wcr.challenger_name} defeated ${wizName}! (+${wcr.score_change} score)` diff --git a/frontend/src/utils/pixelAvatars.tsx b/frontend/src/utils/pixelAvatars.tsx index f3e0302..24c6303 100644 --- a/frontend/src/utils/pixelAvatars.tsx +++ b/frontend/src/utils/pixelAvatars.tsx @@ -438,7 +438,7 @@ export function drawPlayerPiece( ctx.restore(); } -// Canvas Drawing Helper for the Grand Wizard NPC +// Canvas Drawing Helper for the Gary the Wizard NPC export function drawWizardPiece( ctx: CanvasRenderingContext2D, wizard: { x: number; y: number; name: string; strength: number; color?: string },